From 474aa626f01107bc80b7324ed341b9da9359f635 Mon Sep 17 00:00:00 2001 From: vmrajas Date: Wed, 10 Feb 2021 16:41:06 +0530 Subject: [PATCH] Fix(GraphQL): Refactor Mutation Rewriter for Add and Update Mutations (#7409) (#7413) * Feat(GraphQL): Refactor Mutation Rewriting for AddRewriter (#7257) * Add Existence Queries for Mutation Rewriting * Add tests * Add Error handling to getExistenceQueries * Use new code for AddRewriter * Fix Geo * Fix tests * Fix auth tests * Fix resolve tests * Fix add_mutation_test.yaml * Add bug related tests * Address more comments * Rebase * Final changes * Fix(GraphQL): Refactor Mutation Rewriting for Update Rewriter (#7383) * Fix auth tests * Refactor Update Mutation Rewriting * Fix update tests * Add comments and remove unused code * Address comments * Address more comments * Fix ACL test (cherry picked from commit d080743a54346addaffc638e61e776014dab6b1e) --- ee/acl/acl_test.go | 7 +- graphql/admin/add_group.go | 17 +- graphql/admin/update_group.go | 21 +- graphql/e2e/common/common.go | 16 + graphql/e2e/common/error.go | 13 +- graphql/e2e/common/mutation.go | 3716 +++++++++++-------- graphql/e2e/directives/schema.graphql | 32 + graphql/e2e/directives/schema_response.json | 1828 +++++---- graphql/e2e/normal/schema.graphql | 32 + graphql/e2e/normal/schema_response.json | 1827 +++++---- graphql/resolve/add_mutation_test.yaml | 2105 +++++++---- graphql/resolve/auth_add_test.yaml | 261 +- graphql/resolve/auth_delete_test.yaml | 62 +- graphql/resolve/auth_test.go | 110 +- graphql/resolve/auth_update_test.yaml | 127 +- graphql/resolve/extensions_test.go | 12 +- graphql/resolve/mutation.go | 107 +- graphql/resolve/mutation_rewriter.go | 1776 +++++---- graphql/resolve/mutation_test.go | 123 +- graphql/resolve/resolver_error_test.go | 35 +- graphql/resolve/resolver_test.go | 8 +- graphql/resolve/update_mutation_test.yaml | 881 +++-- graphql/schema/wrappers.go | 2 +- 23 files changed, 7455 insertions(+), 5663 deletions(-) diff --git a/ee/acl/acl_test.go b/ee/acl/acl_test.go index c8e5d1f53cd..daccc98bad3 100644 --- a/ee/acl/acl_test.go +++ b/ee/acl/acl_test.go @@ -228,10 +228,9 @@ func TestCreateAndDeleteUsers(t *testing.T) { // adding the user again should fail token := testutil.GrootHttpLogin(adminEndpoint) resp := createUser(t, token, userid, userpassword) - require.Equal(t, x.GqlErrorList{{ - Message: "couldn't rewrite query for mutation addUser because id alice already exists" + - " for type User", - }}, resp.Errors) + require.Equal(t, 1, len(resp.Errors)) + require.Equal(t, "couldn't rewrite mutation addUser because failed to rewrite mutation payload because id"+ + " alice already exists for type User", resp.Errors[0].Message) checkUserCount(t, resp.Data, 0) // delete the user diff --git a/graphql/admin/add_group.go b/graphql/admin/add_group.go index b350df4d3de..dae459fa554 100644 --- a/graphql/admin/add_group.go +++ b/graphql/admin/add_group.go @@ -16,12 +16,25 @@ func NewAddGroupRewriter() resolve.MutationRewriter { return &addGroupRewriter{} } +// RewriteQueries generates and rewrites queries for schema.Mutation +// into dql queries. These queries are used to check if there exist any +// nodes with the ID or XID which we are going to be adding. +// RewriteQueries on addGroupRewriter calls the corresponding function for +// AddRewriter. +func (mrw *addGroupRewriter) RewriteQueries( + ctx context.Context, + m schema.Mutation) ([]*gql.GraphQuery, error) { + + return ((*resolve.AddRewriter)(mrw)).RewriteQueries(ctx, m) +} + // Rewrite rewrites schema.Mutation into dql upsert mutations only for Group type. // It ensures that only the last rule out of all duplicate rules in input is preserved. // A rule is duplicate if it has same predicate name as another rule. func (mrw *addGroupRewriter) Rewrite( ctx context.Context, - m schema.Mutation) ([]*resolve.UpsertMutation, error) { + m schema.Mutation, + idExistence map[string]string) ([]*resolve.UpsertMutation, error) { addGroupInput, _ := m.ArgValue(schema.InputArgName).([]interface{}) @@ -34,7 +47,7 @@ func (mrw *addGroupRewriter) Rewrite( m.SetArgTo(schema.InputArgName, addGroupInput) - return ((*resolve.AddRewriter)(mrw)).Rewrite(ctx, m) + return ((*resolve.AddRewriter)(mrw)).Rewrite(ctx, m, idExistence) } // FromMutationResult rewrites the query part of a GraphQL add mutation into a Dgraph query. diff --git a/graphql/admin/update_group.go b/graphql/admin/update_group.go index 14acdc7d646..ea99216b889 100644 --- a/graphql/admin/update_group.go +++ b/graphql/admin/update_group.go @@ -17,6 +17,19 @@ func NewUpdateGroupRewriter() resolve.MutationRewriter { return &updateGroupRewriter{} } +// RewriteQueries on updateGroupRewriter initializes urw.VarGen and +// urw.XidMetadata. As there is no need to rewrite queries to check for existing +// nodes. It does not rewrite any queries. +func (urw *updateGroupRewriter) RewriteQueries( + ctx context.Context, + m schema.Mutation) ([]*gql.GraphQuery, error) { + + urw.VarGen = resolve.NewVariableGenerator() + urw.XidMetadata = resolve.NewXidMetadata() + + return []*gql.GraphQuery{}, nil +} + // Rewrite rewrites set and remove update patches into dql upsert mutations // only for Group type. It ensures that if a rule already exists in db, it is updated; // otherwise, it is created. It also ensures that only the last rule out of all @@ -24,7 +37,8 @@ func NewUpdateGroupRewriter() resolve.MutationRewriter { // name as another rule. func (urw *updateGroupRewriter) Rewrite( ctx context.Context, - m schema.Mutation) ([]*resolve.UpsertMutation, error) { + m schema.Mutation, + idExistence map[string]string) ([]*resolve.UpsertMutation, error) { inp := m.ArgValue(schema.InputArgName).(map[string]interface{}) setArg := inp["set"] @@ -39,7 +53,6 @@ func (urw *updateGroupRewriter) Rewrite( var errSet, errDel error var mutSet, mutDel []*dgoapi.Mutation - varGen := resolve.NewVariableGenerator() ruleType := m.MutatedType().Field("rules").Type() if setArg != nil { @@ -50,7 +63,7 @@ func (urw *updateGroupRewriter) Rewrite( } for _, ruleI := range rules { rule := ruleI.(map[string]interface{}) - variable := varGen.Next(ruleType, "", "", false) + variable := urw.VarGen.Next(ruleType, "", "", false) predicate := rule["predicate"] permission := rule["permission"] @@ -96,7 +109,7 @@ func (urw *updateGroupRewriter) Rewrite( continue } - variable := varGen.Next(ruleType, "", "", false) + variable := urw.VarGen.Next(ruleType, "", "", false) addAclRuleQuery(upsertQuery, predicate.(string), variable) deleteJson := []byte(fmt.Sprintf(`[ diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index 7e23ba1ac27..99ddf49e17b 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -178,6 +178,18 @@ type state struct { Code string `json:"xcode,omitempty"` Capital string `json:"capital,omitempty"` Country *country `json:"country,omitempty"` + Region *region `json:"region,omitempty"` +} + +type region struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + District *district `json:"district,omitempty"` +} + +type district struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } type movie struct { @@ -702,6 +714,10 @@ func RunAll(t *testing.T) { t.Run("Geo - MultiPolygon type", mutationMultiPolygonType) t.Run("filter in mutations with array for AND/OR", filterInMutationsWithArrayForAndOr) t.Run("filter in update mutations with array for AND/OR", filterInUpdateMutationsWithFilterAndOr) + t.Run("three level double XID mutation", threeLevelDoubleXID) + t.Run("two levels linked to one XID", twoLevelsLinkedToXID) + t.Run("cyclically linked mutation", cyclicMutation) + t.Run("parallel mutations", parallelMutations) // error tests t.Run("graphql completion on", graphQLCompletionOn) diff --git a/graphql/e2e/common/error.go b/graphql/e2e/common/error.go index a236d4cf7fc..9ee06944eb1 100644 --- a/graphql/e2e/common/error.go +++ b/graphql/e2e/common/error.go @@ -134,8 +134,8 @@ func deepMutationErrors(t *testing.T) { }{ "missing ID and XID": { set: &country{States: []*state{{Name: "NOT A VALID STATE"}}}, - exp: "couldn't rewrite mutation updateCountry because failed to rewrite mutation " + - "payload because type State requires a value for field xcode, but no value present", + exp: "couldn't rewrite mutation updateCountry because failed to rewrite" + + " mutation payload because field xcode cannot be empty", }, "ID not valid": { set: &country{States: []*state{{ID: "HI"}}}, @@ -144,13 +144,14 @@ func deepMutationErrors(t *testing.T) { }, "ID not found": { set: &country{States: []*state{{ID: "0x1"}}}, - exp: "couldn't rewrite query for mutation updateCountry because ID \"0x1\" isn't a State", + exp: "couldn't rewrite mutation updateCountry because failed to rewrite mutation" + + " payload because ID \"0x1\" isn't a State", }, "XID not found": { set: &country{States: []*state{{Code: "NOT A VALID CODE"}}}, - exp: "couldn't rewrite query for mutation updateCountry because xid " + - "\"NOT A VALID CODE\" doesn't exist and input object not well formed because type " + - "State requires a value for field name, but no value present", + exp: "couldn't rewrite mutation updateCountry because failed to rewrite mutation" + + " payload because type State requires a value for field name, but no value" + + " present", }, } diff --git a/graphql/e2e/common/mutation.go b/graphql/e2e/common/mutation.go index 818d340cc92..74125afa21b 100644 --- a/graphql/e2e/common/mutation.go +++ b/graphql/e2e/common/mutation.go @@ -25,6 +25,7 @@ import ( "encoding/json" "fmt" "sort" + "sync" "testing" "github.com/dgraph-io/dgo/v200" @@ -82,17 +83,17 @@ func add(t *testing.T, executeRequest requestExecutor) { func addCountry(t *testing.T, executeRequest requestExecutor) *country { addCountryParams := &GraphQLParams{ Query: `mutation addCountry($name: String!) { - addCountry(input: [{ name: $name }]) { - country { - id - name - } - } - }`, + addCountry(input: [{ name: $name }]) { + country { + id + name + } + } + }`, Variables: map[string]interface{}{"name": "Testland"}, } addCountryExpected := ` - { "addCountry": { "country": [{ "id": "_UID_", "name": "Testland" }] } }` + { "addCountry": { "country": [{ "id": "_UID_", "name": "Testland" }] } }` gqlResponse := executeRequest(t, GraphqlURL, addCountryParams) RequireNoGQLErrors(t, gqlResponse) @@ -127,16 +128,16 @@ func requireCountry(t *testing.T, uid string, expectedCountry *country, includeS params := &GraphQLParams{ Query: `query getCountry($id: ID!, $includeStates: Boolean!) { - getCountry(id: $id) { - id - name - states(order: { asc: xcode }) @include(if: $includeStates) { - id - xcode - name - } - } - }`, + getCountry(id: $id) { + id + name + states(order: { asc: xcode }) @include(if: $includeStates) { + id + xcode + name + } + } + }`, Variables: map[string]interface{}{"id": uid, "includeStates": includeStates}, } gqlResponse := executeRequest(t, GraphqlURL, params) @@ -158,23 +159,23 @@ func addAuthor(t *testing.T, countryUID string, addAuthorParams := &GraphQLParams{ Query: `mutation addAuthor($author: AddAuthorInput!) { - addAuthor(input: [$author]) { - author { - id - name - dob - reputation - country { - id - name - } - posts { - title - text - } - } - } - }`, + addAuthor(input: [$author]) { + author { + id + name + dob + reputation + country { + id + name + } + posts { + title + text + } + } + } + }`, Variables: map[string]interface{}{"author": map[string]interface{}{ "name": "Test Author", "dob": "2010-01-01T05:04:33Z", @@ -184,18 +185,18 @@ func addAuthor(t *testing.T, countryUID string, } addAuthorExpected := fmt.Sprintf(`{ "addAuthor": { - "author": [{ - "id": "_UID_", - "name": "Test Author", - "dob": "2010-01-01T05:04:33Z", - "reputation": 7.75, - "country": { - "id": "%s", - "name": "Testland" - }, - "posts": [] - }] - } }`, countryUID) + "author": [{ + "id": "_UID_", + "name": "Test Author", + "dob": "2010-01-01T05:04:33Z", + "reputation": 7.75, + "country": { + "id": "%s", + "name": "Testland" + }, + "posts": [] + }] + } }`, countryUID) gqlResponse := executeRequest(t, GraphqlURL, addAuthorParams) RequireNoGQLErrors(t, gqlResponse) @@ -226,27 +227,27 @@ func requireAuthor(t *testing.T, authorID string, expectedAuthor *author, params := &GraphQLParams{ Query: `query getAuthor($id: ID!) { - getAuthor(id: $id) { - id - name - dob - reputation - country { - id - name - } - posts(order: { asc: title }) { - postID - title - text - tags - category { - id - name - } - } - } - }`, + getAuthor(id: $id) { + id + name + dob + reputation + country { + id + name + } + posts(order: { asc: title }) { + postID + title + text + tags + category { + id + name + } + } + } + }`, Variables: map[string]interface{}{"id": authorID}, } gqlResponse := executeRequest(t, GraphqlURL, params) @@ -266,17 +267,17 @@ func requireAuthor(t *testing.T, authorID string, expectedAuthor *author, func addCategory(t *testing.T, executeRequest requestExecutor) *category { addCategoryParams := &GraphQLParams{ Query: `mutation addCategory($name: String!) { - addCategory(input: [{ name: $name }]) { - category { - id - name - } - } - }`, + addCategory(input: [{ name: $name }]) { + category { + id + name + } + } + }`, Variables: map[string]interface{}{"name": "A Category"}, } addCategoryExpected := ` - { "addCategory": { "category": [{ "id": "_UID_", "name": "A Category" }] } }` + { "addCategory": { "category": [{ "id": "_UID_", "name": "A Category" }] } }` gqlResponse := executeRequest(t, GraphqlURL, addCategoryParams) RequireNoGQLErrors(t, gqlResponse) @@ -366,33 +367,33 @@ func deepMutationsTest(t *testing.T, executeRequest requestExecutor) { updateAuthorParams := &GraphQLParams{ Query: `mutation updateAuthor($id: ID!, $set: AuthorPatch!, $remove: AuthorPatch!) { - updateAuthor( - input: { - filter: {id: [$id]}, - set: $set, - remove: $remove - } - ) { - author { - id - name - country { - id - name - } - posts { - postID - title - text - tags - category { - id - name - } - } - } - } - }`, + updateAuthor( + input: { + filter: {id: [$id]}, + set: $set, + remove: $remove + } + ) { + author { + id + name + country { + id + name + } + posts { + postID + title + text + tags + category { + id + name + } + } + } + } + }`, Variables: map[string]interface{}{ "id": newAuth.ID, "set": patchSet, @@ -511,29 +512,29 @@ func addMultipleAuthorFromRef(t *testing.T, newAuthor []*author, executeRequest requestExecutor) []*author { addAuthorParams := &GraphQLParams{ Query: `mutation addAuthor($author: [AddAuthorInput!]!) { - addAuthor(input: $author) { - author { - id - name - qualification - reputation - country { - id - name - } - posts(order: { asc: title }) { - postID - title - text - tags - category { - id - name - } - } - } - } - }`, + addAuthor(input: $author) { + author { + id + name + qualification + reputation + country { + id + name + } + posts(order: { asc: title }) { + postID + title + text + tags + category { + id + name + } + } + } + } + }`, Variables: map[string]interface{}{"author": newAuthor}, } @@ -576,12 +577,12 @@ func addComments(t *testing.T, ids []string) { params := &GraphQLParams{ Query: `mutation($input: [AddComment1Input!]!) { - addComment1(input: $input) { - comment1 { - id - } - } - }`, + addComment1(input: $input) { + comment1 { + id + } + } + }`, Variables: map[string]interface{}{ "input": input, }, @@ -594,35 +595,35 @@ func addComments(t *testing.T, ids []string) { func testThreeLevelXID(t *testing.T) { input := `{ - "input": [ - { - "id": "post1", - "comments": [ - { - "id": "comment1", - "replies": [ - { - "id": "reply1" - } - ] - } - ] - }, - { - "id": "post2", - "comments": [ - { - "id": "comment2", - "replies": [ - { - "id": "reply1" - } - ] - } - ] - } - ] - }` + "input": [ + { + "id": "post1", + "comments": [ + { + "id": "comment1", + "replies": [ + { + "id": "reply1" + } + ] + } + ] + }, + { + "id": "post2", + "comments": [ + { + "id": "comment2", + "replies": [ + { + "id": "reply1" + } + ] + } + ] + } + ] + }` qinput := make(map[string]interface{}) err := json.Unmarshal([]byte(input), &qinput) @@ -630,136 +631,136 @@ func testThreeLevelXID(t *testing.T) { addPostParams := &GraphQLParams{ Query: ` mutation($input: [AddPost1Input!]!) { - addPost1(input: $input) { - post1(order: { asc: id }) { - id - comments { - id - replies { - id - } - } - } - } - }`, + addPost1(input: $input) { + post1(order: { asc: id }) { + id + comments { + id + replies { + id + } + } + } + } + }`, Variables: qinput, } bothCommentsLinkedToReply := `{ - "addPost1": { - "post1": [ - { - "id": "post1", - "comments": [ - { - "id": "comment1", - "replies": [ - { - "id": "reply1" - } - ] - } - ] - }, - { - "id": "post2", - "comments": [ - { - "id": "comment2", - "replies": [ - { - "id": "reply1" - } - ] - } - ] - } - ] - } - }` + "addPost1": { + "post1": [ + { + "id": "post1", + "comments": [ + { + "id": "comment1", + "replies": [ + { + "id": "reply1" + } + ] + } + ] + }, + { + "id": "post2", + "comments": [ + { + "id": "comment2", + "replies": [ + { + "id": "reply1" + } + ] + } + ] + } + ] + } + }` firstCommentLinkedToReply := `{ - "addPost1": { - "post1": [ - { - "id": "post1", - "comments": [ - { - "id": "comment1", - "replies": [ - { - "id": "reply1" - } - ] - } - ] - }, - { - "id": "post2", - "comments": [ - { - "id": "comment2", - "replies": [] - } - ] - } - ] - } - }` + "addPost1": { + "post1": [ + { + "id": "post1", + "comments": [ + { + "id": "comment1", + "replies": [ + { + "id": "reply1" + } + ] + } + ] + }, + { + "id": "post2", + "comments": [ + { + "id": "comment2", + "replies": [] + } + ] + } + ] + } + }` secondCommentLinkedToReply := `{ - "addPost1": { - "post1": [ - { - "id": "post1", - "comments": [ - { - "id": "comment1", - "replies": [] - } - ] - }, - { - "id": "post2", - "comments": [ - { - "id": "comment2", - "replies": [ - { - "id": "reply1" - } - ] - } - ] - } - ] - } - }` + "addPost1": { + "post1": [ + { + "id": "post1", + "comments": [ + { + "id": "comment1", + "replies": [] + } + ] + }, + { + "id": "post2", + "comments": [ + { + "id": "comment2", + "replies": [ + { + "id": "reply1" + } + ] + } + ] + } + ] + } + }` noCommentsLinkedToReply := `{ - "addPost1": { - "post1": [ - { - "id": "post1", - "comments": [ - { - "id": "comment1", - "replies": [] - } - ] - }, - { - "id": "post2", - "comments": [ - { - "id": "comment2", - "replies": [] - } - ] - } - ] - } - }` + "addPost1": { + "post1": [ + { + "id": "post1", + "comments": [ + { + "id": "comment1", + "replies": [] + } + ] + }, + { + "id": "post2", + "comments": [ + { + "id": "comment2", + "replies": [] + } + ] + } + ] + } + }` cases := map[string]struct { Comments []string @@ -835,23 +836,23 @@ func deepXIDTest(t *testing.T, executeRequest requestExecutor) { // sets up the "XZY" xid that's used by the following mutation. addCountryParams := &GraphQLParams{ Query: `mutation addCountry($input: AddCountryInput!) { - addState(input: [{ xcode: "XYZ", name: "A State" }]) { - state { id xcode name } - } - - addCountry(input: [$input]) - { - country { - id - name - states(order: { asc: xcode }) { - id - xcode - name - } - } - } - }`, + addState(input: [{ xcode: "XYZ", name: "A State" }]) { + state { id xcode name } + } + + addCountry(input: [$input]) + { + country { + id + name + states(order: { asc: xcode }) { + id + xcode + name + } + } + } + }`, Variables: map[string]interface{}{"input": newCountry}, } @@ -896,28 +897,28 @@ func deepXIDTest(t *testing.T, executeRequest requestExecutor) { updateCountryParams := &GraphQLParams{ Query: `mutation updateCountry($id: ID!, $set: CountryPatch!, $remove: CountryPatch!) { - addState(input: [{ xcode: "DEF", name: "Definitely A State" }]) { - state { id } - } - - updateCountry( - input: { - filter: {id: [$id]}, - set: $set, - remove: $remove - } - ) { - country { - id - name - states(order: { asc: xcode }) { - id - xcode - name - } - } - } - }`, + addState(input: [{ xcode: "DEF", name: "Definitely A State" }]) { + state { id } + } + + updateCountry( + input: { + filter: {id: [$id]}, + set: $set, + remove: $remove + } + ) { + country { + id + name + states(order: { asc: xcode }) { + id + xcode + name + } + } + } + }`, Variables: map[string]interface{}{ "id": addResult.AddCountry.Country[0].ID, "set": patchSet, @@ -961,26 +962,26 @@ func addPost(t *testing.T, authorID, countryID string, addPostParams := &GraphQLParams{ Query: `mutation addPost($post: AddPostInput!) { - addPost(input: [$post]) { - post { - postID - title - text - isPublished - tags - numLikes - numViews - author { - id - name - country { - id - name - } - } - } - } - }`, + addPost(input: [$post]) { + post { + postID + title + text + isPublished + tags + numLikes + numViews + author { + id + name + country { + id + name + } + } + } + } + }`, Variables: map[string]interface{}{"post": map[string]interface{}{ "title": "Test Post", "text": "This post is just a test.", @@ -993,24 +994,24 @@ func addPost(t *testing.T, authorID, countryID string, } addPostExpected := fmt.Sprintf(`{ "addPost": { - "post": [{ - "postID": "_UID_", - "title": "Test Post", - "text": "This post is just a test.", - "isPublished": true, - "tags": ["example", "test"], - "numLikes": 1000, - "numViews": 9007199254740991, - "author": { - "id": "%s", - "name": "Test Author", - "country": { - "id": "%s", - "name": "Testland" - } - } - }] - } }`, authorID, countryID) + "post": [{ + "postID": "_UID_", + "title": "Test Post", + "text": "This post is just a test.", + "isPublished": true, + "tags": ["example", "test"], + "numLikes": 1000, + "numViews": 9007199254740991, + "author": { + "id": "%s", + "name": "Test Author", + "country": { + "id": "%s", + "name": "Testland" + } + } + }] + } }`, authorID, countryID) gqlResponse := executeRequest(t, GraphqlURL, addPostParams) RequireNoGQLErrors(t, gqlResponse) @@ -1040,24 +1041,24 @@ func addPostWithNullText(t *testing.T, authorID, countryID string, addPostParams := &GraphQLParams{ Query: `mutation addPost($post: AddPostInput!) { - addPost(input: [$post]) { - post( filter : {not :{has : text} }){ - postID - title - text - isPublished - tags - author(filter: {has:country}) { - id - name - country { - id - name - } - } - } - } - }`, + addPost(input: [$post]) { + post( filter : {not :{has : text} }){ + postID + title + text + isPublished + tags + author(filter: {has:country}) { + id + name + country { + id + name + } + } + } + } + }`, Variables: map[string]interface{}{"post": map[string]interface{}{ "title": "No text", "isPublished": false, @@ -1068,23 +1069,23 @@ func addPostWithNullText(t *testing.T, authorID, countryID string, } addPostExpected := fmt.Sprintf(`{ "addPost": { - "post": [{ - "postID": "_UID_", - "title": "No text", - "text": null, - "isPublished": false, - "tags": ["null","no text"], - "numLikes": 0, - "author": { - "id": "%s", - "name": "Test Author", - "country": { - "id": "%s", - "name": "Testland" - } - } - }] - } }`, authorID, countryID) + "post": [{ + "postID": "_UID_", + "title": "No text", + "text": null, + "isPublished": false, + "tags": ["null","no text"], + "numLikes": 0, + "author": { + "id": "%s", + "name": "Test Author", + "country": { + "id": "%s", + "name": "Testland" + } + } + }] + } }`, authorID, countryID) gqlResponse := executeRequest(t, GraphqlURL, addPostParams) RequireNoGQLErrors(t, gqlResponse) @@ -1118,24 +1119,24 @@ func requirePost( params := &GraphQLParams{ Query: `query getPost($id: ID!, $getAuthor: Boolean!) { - getPost(postID: $id) { - postID - title - text - isPublished - tags - numLikes - numViews - author @include(if: $getAuthor) { - id - name - country { - id - name - } - } - } - }`, + getPost(postID: $id) { + postID + title + text + isPublished + tags + numLikes + numViews + author @include(if: $getAuthor) { + id + name + country { + id + name + } + } + } + }`, Variables: map[string]interface{}{ "id": postID, "getAuthor": getAuthor, @@ -1244,15 +1245,15 @@ func updateRemove(t *testing.T) { updateParams := &GraphQLParams{ Query: `mutation updPost($filter: PostFilter!, $rem: PostPatch!) { - updatePost(input: { filter: $filter, remove: $rem }) { - post { - text - isPublished - tags - numLikes - } - } - }`, + updatePost(input: { filter: $filter, remove: $rem }) { + post { + text + isPublished + tags + numLikes + } + } + }`, Variables: map[string]interface{}{"filter": filter, "rem": remPatch}, } @@ -1260,17 +1261,17 @@ func updateRemove(t *testing.T) { RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, `{ - "updatePost": { - "post": [ - { - "text": null, - "isPublished": null, - "tags": ["example"], - "numLikes": 1000 - } - ] - } - }`, + "updatePost": { + "post": [ + { + "text": null, + "isPublished": null, + "tags": ["example"], + "numLikes": 1000 + } + ] + } + }`, string([]byte(gqlResponse.Data))) newPost.Text = "" // was deleted because the given val was correct @@ -1285,13 +1286,13 @@ func updateRemove(t *testing.T) { func updateCountry(t *testing.T, filter map[string]interface{}, newName string, shouldUpdate bool) { updateParams := &GraphQLParams{ Query: `mutation newName($filter: CountryFilter!, $newName: String!) { - updateCountry(input: { filter: $filter, set: { name: $newName } }) { - country { - id - name - } - } - }`, + updateCountry(input: { filter: $filter, set: { name: $newName } }) { + country { + id + name + } + } + }`, Variables: map[string]interface{}{"filter": filter, "newName": newName}, } @@ -1362,14 +1363,14 @@ func filterInUpdate(t *testing.T) { t.Run(name, func(t *testing.T) { updateParams := &GraphQLParams{ Query: `mutation newName($filter: CountryFilter!, $newName: String!, - $filterCountries: CountryFilter!) { - updateCountry(input: { filter: $filter, set: { name: $newName } }) { - country(filter: $filterCountries) { - id - name - } - } - }`, + $filterCountries: CountryFilter!) { + updateCountry(input: { filter: $filter, set: { name: $newName } }) { + country(filter: $filterCountries) { + id + name + } + } + }`, Variables: map[string]interface{}{ "filter": test.Filter, "newName": "updatedValue", @@ -1473,10 +1474,10 @@ func addMutationUpdatesRefs(t *testing.T, executeRequest requestExecutor) { // post and the author it was originally linked to. addAuthorParams := &GraphQLParams{ Query: `mutation addAuthor($author: AddAuthorInput!) { - addAuthor(input: [$author]) { - author { id } - } - }`, + addAuthor(input: [$author]) { + author { id } + } + }`, Variables: map[string]interface{}{"author": map[string]interface{}{ "name": "Test Author", "posts": []interface{}{newPost}, @@ -1514,18 +1515,18 @@ func addMutationUpdatesRefsXID(t *testing.T, executeRequest requestExecutor) { // The addCountry2 mutation should also remove the state "ABC" from country1's states list addCountryParams := &GraphQLParams{ Query: `mutation addCountry($input: AddCountryInput!) { - addCountry1: addCountry(input: [$input]) { - country { id } - } - addCountry2: addCountry(input: [$input]) { - country { - id - states { - id - } - } - } - }`, + addCountry1: addCountry(input: [$input]) { + country { id } + } + addCountry2: addCountry(input: [$input]) { + country { + id + states { + id + } + } + } + }`, Variables: map[string]interface{}{"input": newCountry}, } @@ -1574,15 +1575,15 @@ func updateMutationUpdatesRefs(t *testing.T, executeRequest requestExecutor) { // from author1's post list updateAuthorParams := &GraphQLParams{ Query: `mutation updateAuthor($id: ID!, $set: AuthorPatch!) { - updateAuthor( - input: { - filter: {id: [$id]}, - set: $set - } - ) { - author { id } - } - }`, + updateAuthor( + input: { + filter: {id: [$id]}, + set: $set + } + ) { + author { id } + } + }`, Variables: map[string]interface{}{ "id": newAuthor2.ID, "set": map[string]interface{}{"posts": []interface{}{newPost}}, @@ -1619,19 +1620,19 @@ func updateMutationOnlyUpdatesRefsIfDifferent(t *testing.T, executeRequest reque // the only change should be in the post text updateAuthorParams := &GraphQLParams{ Query: `mutation updatePost($id: ID!, $set: PostPatch!) { - updatePost( - input: { - filter: {postID: [$id]}, - set: $set - } - ) { - post { - postID - text - author { id } - } - } - }`, + updatePost( + input: { + filter: {postID: [$id]}, + set: $set + } + ) { + post { + postID + text + author { id } + } + } + }`, Variables: map[string]interface{}{ "id": newPost.PostID, "set": map[string]interface{}{ @@ -1646,13 +1647,13 @@ func updateMutationOnlyUpdatesRefsIfDifferent(t *testing.T, executeRequest reque // The text is updated as expected // The author is unchanged expected := fmt.Sprintf(` - { "updatePost": { "post": [ - { - "postID": "%s", - "text": "The Updated Text", - "author": { "id": "%s" } - } - ] } }`, newPost.PostID, newAuthor.ID) + { "updatePost": { "post": [ + { + "postID": "%s", + "text": "The Updated Text", + "author": { "id": "%s" } + } + ] } }`, newPost.PostID, newAuthor.ID) require.JSONEq(t, expected, string(gqlResponse.Data)) @@ -1670,10 +1671,10 @@ func updateMutationUpdatesRefsXID(t *testing.T, executeRequest requestExecutor) addCountryParams := &GraphQLParams{ Query: `mutation addCountry($input: AddCountryInput!) { - addCountry(input: [$input]) { - country { id } - } - }`, + addCountry(input: [$input]) { + country { id } + } + }`, Variables: map[string]interface{}{"input": newCountry}, } @@ -1696,15 +1697,15 @@ func updateMutationUpdatesRefsXID(t *testing.T, executeRequest requestExecutor) updateCountryParams := &GraphQLParams{ Query: `mutation updateCountry($id: ID!, $set: CountryPatch!) { - updateCountry( - input: { - filter: {id: [$id]}, - set: $set - } - ) { - country { id } - } - }`, + updateCountry( + input: { + filter: {id: [$id]}, + set: $set + } + ) { + country { id } + } + }`, Variables: map[string]interface{}{ "id": newCountry2.ID, "set": map[string]interface{}{"states": newCountry.States}, @@ -1743,15 +1744,15 @@ func deleteMutationSingleReference(t *testing.T, executeRequest requestExecutor) addCountryParams := &GraphQLParams{ Query: `mutation addCountry($input: AddCountryInput!) { - addCountry(input: [$input]) { - country { - id - states { - id - } - } - } - }`, + addCountry(input: [$input]) { + country { + id + states { + id + } + } + } + }`, Variables: map[string]interface{}{"input": newCountry}, } @@ -1773,10 +1774,10 @@ func deleteMutationSingleReference(t *testing.T, executeRequest requestExecutor) // the state doesn't belong to a country getCatParams := &GraphQLParams{ Query: `query getState($id: ID!) { - getState(id: $id) { - country { id } - } - }`, + getState(id: $id) { + country { id } + } + }`, Variables: map[string]interface{}{"id": addResult.AddCountry.Country[0].States[0].ID}, } gqlResponse = getCatParams.ExecuteAsPost(t, GraphqlURL) @@ -1793,10 +1794,10 @@ func deleteMutationMultipleReferences(t *testing.T, executeRequest requestExecut updateParams := &GraphQLParams{ Query: `mutation updPost($filter: PostFilter!, $set: PostPatch!) { - updatePost(input: { filter: $filter, set: $set }) { - post { postID category { id } } - } - }`, + updatePost(input: { filter: $filter, set: $set }) { + post { postID category { id } } + } + }`, Variables: map[string]interface{}{ "filter": map[string]interface{}{"postID": []string{newPost.PostID}}, "set": map[string]interface{}{"category": newCategory}}, @@ -1824,10 +1825,10 @@ func deleteMutationMultipleReferences(t *testing.T, executeRequest requestExecut // the category doesn't have any posts getCatParams := &GraphQLParams{ Query: `query getCategory($id: ID!) { - getCategory(id: $id) { - posts { postID } - } - }`, + getCategory(id: $id) { + posts { postID } + } + }`, Variables: map[string]interface{}{"id": newCategory.ID}, } gqlResponse = getCatParams.ExecuteAsPost(t, GraphqlURL) @@ -1869,18 +1870,18 @@ func deleteWrongID(t *testing.T) { newAuthor := addAuthor(t, newCountry.ID, postExecutor) expectedData := `{ "deleteCountry": { - "msg": "No nodes were deleted", - "numUids": 0 - } }` + "msg": "No nodes were deleted", + "numUids": 0 + } }` filter := map[string]interface{}{"id": []string{newAuthor.ID}} deleteCountryParams := &GraphQLParams{ Query: `mutation deleteCountry($filter: CountryFilter!) { - deleteCountry(filter: $filter) { - msg - numUids - } - }`, + deleteCountry(filter: $filter) { + msg + numUids + } + }`, Variables: map[string]interface{}{"filter": filter}, } @@ -1894,31 +1895,31 @@ func manyMutations(t *testing.T) { newCountry := addCountry(t, postExecutor) multiMutationParams := &GraphQLParams{ Query: `mutation addCountries($name1: String!, $filter: CountryFilter!, $name2: String!) { - add1: addCountry(input: [{ name: $name1 }]) { - country { - id - name - } - } - - deleteCountry(filter: $filter) { msg } - - add2: addCountry(input: [{ name: $name2 }]) { - country { - id - name - } - } - }`, + add1: addCountry(input: [{ name: $name1 }]) { + country { + id + name + } + } + + deleteCountry(filter: $filter) { msg } + + add2: addCountry(input: [{ name: $name2 }]) { + country { + id + name + } + } + }`, Variables: map[string]interface{}{ "name1": "Testland1", "filter": map[string]interface{}{ "id": []string{newCountry.ID}}, "name2": "Testland2"}, } multiMutationExpected := `{ - "add1": { "country": [{ "id": "_UID_", "name": "Testland1" }] }, - "deleteCountry" : { "msg": "Deleted" }, - "add2": { "country": [{ "id": "_UID_", "name": "Testland2" }] } - }` + "add1": { "country": [{ "id": "_UID_", "name": "Testland1" }] }, + "deleteCountry" : { "msg": "Deleted" }, + "add2": { "country": [{ "id": "_UID_", "name": "Testland2" }] } + }` gqlResponse := multiMutationParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) @@ -2000,14 +2001,14 @@ func testSelectionInAddObject(t *testing.T) { t.Run(name, func(t *testing.T) { addPostParams := &GraphQLParams{ Query: `mutation addPost($posts: [AddPostInput!]!, $filter: - PostFilter, $first: Int, $offset: Int, $sort: PostOrder) { - addPost(input: $posts) { - post (first:$first, offset:$offset, filter:$filter, order:$sort){ - postID - title - } - } - }`, + PostFilter, $first: Int, $offset: Int, $sort: PostOrder) { + addPost(input: $posts) { + post (first:$first, offset:$offset, filter:$filter, order:$sort){ + postID + title + } + } + }`, Variables: map[string]interface{}{ "posts": []*post{post1, post2}, "first": test.First, @@ -2046,21 +2047,21 @@ func mutationEmptyDelete(t *testing.T) { // Try to delete a node that doesn't exists. updatePostParams := &GraphQLParams{ Query: `mutation{ - updatePost(input:{ - filter:{title:{allofterms:"Random"}}, - remove:{author:{name:"Non Existent"}} - }) { - post { - title - } - } - }`, + updatePost(input:{ + filter:{title:{allofterms:"Random"}}, + remove:{author:{name:"Non Existent"}} + }) { + post { + title + } + } + }`, } gqlResponse := updatePostParams.ExecuteAsPost(t, GraphqlURL) require.NotNil(t, gqlResponse.Errors) - require.Equal(t, gqlResponse.Errors[0].Error(), "couldn't rewrite mutation updatePost"+ - " because failed to rewrite mutation payload because id is not provided") + require.Equal(t, "couldn't rewrite mutation updatePost because failed to"+ + " rewrite mutation payload because id is not provided", gqlResponse.Errors[0].Error()) } // After a successful mutation, the following query is executed. That query can @@ -2079,17 +2080,17 @@ func mutationWithDeepFilter(t *testing.T) { addPostParams := &GraphQLParams{ Query: `mutation addPost($post: AddPostInput!) { - addPost(input: [$post]) { - post { - postID - author { - posts(filter: { title: { allofterms: "find me" }}) { - title - } - } - } - } - }`, + addPost(input: [$post]) { + post { + postID + author { + posts(filter: { title: { allofterms: "find me" }}) { + title + } + } + } + } + }`, Variables: map[string]interface{}{"post": map[string]interface{}{ "title": "find me : a test of deep search after mutation", "author": map[string]interface{}{"id": newAuthor.ID}, @@ -2098,13 +2099,13 @@ func mutationWithDeepFilter(t *testing.T) { // Expect the filter to find just the new post, not any of the author's existing posts. addPostExpected := `{ "addPost": { - "post": [{ - "postID": "_UID_", - "author": { - "posts": [ { "title": "find me : a test of deep search after mutation" } ] - } - }] - } }` + "post": [{ + "postID": "_UID_", + "author": { + "posts": [ { "title": "find me : a test of deep search after mutation" } ] + } + }] + } }` gqlResponse := addPostParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) @@ -2159,46 +2160,46 @@ func manyMutationsWithQueryError(t *testing.T) { // add3 - should succeed multiMutationParams := &GraphQLParams{ Query: `mutation addCountries($countryID: ID!) { - add1: addAuthor(input: [{ name: "A. N. Author", country: { id: $countryID }}]) { - author { - id - name - country { - id - } - } - } - - add2: addAuthor(input: [{ name: "Ann Other Author", country: { id: $countryID }}]) { - author { - id - name - country { - id - name - } - } - } - - add3: addCountry(input: [{ name: "abc" }]) { - country { - id - name - } - } - }`, + add1: addAuthor(input: [{ name: "A. N. Author", country: { id: $countryID }}]) { + author { + id + name + country { + id + } + } + } + + add2: addAuthor(input: [{ name: "Ann Other Author", country: { id: $countryID }}]) { + author { + id + name + country { + id + name + } + } + } + + add3: addCountry(input: [{ name: "abc" }]) { + country { + id + name + } + } + }`, Variables: map[string]interface{}{"countryID": newCountry.ID}, } expectedData := fmt.Sprintf(`{ - "add1": { "author": [{ "id": "_UID_", "name": "A. N. Author", "country": { "id": "%s" } }] }, - "add2": { "author": [{ "id": "_UID_", "name": "Ann Other Author", "country": null }] }, - "add3": { "country": [{ "id": "_UID_", "name": "abc" }] } - }`, newCountry.ID) + "add1": { "author": [{ "id": "_UID_", "name": "A. N. Author", "country": { "id": "%s" } }] }, + "add2": { "author": [{ "id": "_UID_", "name": "Ann Other Author", "country": null }] }, + "add3": { "country": [{ "id": "_UID_", "name": "abc" }] } + }`, newCountry.ID) expectedErrors := x.GqlErrorList{ &x.GqlError{Message: `Non-nullable field 'name' (type String!) was not present ` + `in result from Dgraph. GraphQL error propagation triggered.`, - Locations: []x.Location{{Line: 18, Column: 7}}, + Locations: []x.Location{{Line: 18, Column: 25}}, Path: []interface{}{"add2", "author", float64(0), "country", "name"}}} gqlResponse := multiMutationParams.ExecuteAsPost(t, GraphqlURL) @@ -2262,14 +2263,14 @@ type starship struct { func addStarship(t *testing.T) *starship { addStarshipParams := &GraphQLParams{ Query: `mutation addStarship($starship: AddStarshipInput!) { - addStarship(input: [$starship]) { - starship { - id - name - length - } - } - }`, + addStarship(input: [$starship]) { + starship { + id + name + length + } + } + }`, Variables: map[string]interface{}{"starship": map[string]interface{}{ "name": "Millennium Falcon", "length": 2, @@ -2280,11 +2281,11 @@ func addStarship(t *testing.T) *starship { RequireNoGQLErrors(t, gqlResponse) addStarshipExpected := `{"addStarship":{ - "starship":[{ - "name":"Millennium Falcon", - "length":2 - }] - }}` + "starship":[{ + "name":"Millennium Falcon", + "length":2 + }] + }}` var expected, result struct { AddStarship struct { @@ -2309,12 +2310,12 @@ func addStarship(t *testing.T) *starship { func addHuman(t *testing.T, starshipID string) string { addHumanParams := &GraphQLParams{ Query: `mutation addHuman($human: AddHumanInput!) { - addHuman(input: [$human]) { - human { - id - } - } - }`, + addHuman(input: [$human]) { + human { + id + } + } + }`, Variables: map[string]interface{}{"human": map[string]interface{}{ "name": "Han", "ename": "Han_employee", @@ -2346,12 +2347,12 @@ func addHuman(t *testing.T, starshipID string) string { func addDroid(t *testing.T) string { addDroidParams := &GraphQLParams{ Query: `mutation addDroid($droid: AddDroidInput!) { - addDroid(input: [$droid]) { - droid { - id - } - } - }`, + addDroid(input: [$droid]) { + droid { + id + } + } + }`, Variables: map[string]interface{}{"droid": map[string]interface{}{ "name": "R2-D2", "primaryFunction": "Robot", @@ -2379,12 +2380,12 @@ func addDroid(t *testing.T) string { func addThingOne(t *testing.T) string { addThingOneParams := &GraphQLParams{ Query: `mutation addThingOne($input: AddThingOneInput!) { - addThingOne(input: [$input]) { - thingOne { - id - } - } - }`, + addThingOne(input: [$input]) { + thingOne { + id + } + } + }`, Variables: map[string]interface{}{"input": map[string]interface{}{ "name": "Thing-1", "color": "White", @@ -2412,12 +2413,12 @@ func addThingOne(t *testing.T) string { func addThingTwo(t *testing.T) string { addThingTwoParams := &GraphQLParams{ Query: `mutation addThingTwo($input: AddThingTwoInput!) { - addThingTwo(input: [$input]) { - thingTwo { - id - } - } - }`, + addThingTwo(input: [$input]) { + thingTwo { + id + } + } + }`, Variables: map[string]interface{}{"input": map[string]interface{}{ "name": "Thing-2", "color": "Black", @@ -2445,24 +2446,24 @@ func addThingTwo(t *testing.T) string { func addHome(t *testing.T, humanId string) (string, string, string, string) { addHomeParams := &GraphQLParams{ Query: `mutation addHome($input: AddHomeInput!) { - addHome(input: [$input]) { - home { - id - members { - __typename - ... on Animal { - id - } - ... on Human { - id - } - ... on Plant { - id - } - } - } - } - }`, + addHome(input: [$input]) { + home { + id + members { + __typename + ... on Animal { + id + } + ... on Human { + id + } + ... on Plant { + id + } + } + } + } + }`, Variables: map[string]interface{}{ "input": map[string]interface{}{ "address": "Avenger Street", @@ -2560,12 +2561,12 @@ func deleteThingTwo(t *testing.T, thingTwoId string) { func updateCharacter(t *testing.T, id string) { updateCharacterParams := &GraphQLParams{ Query: `mutation updateCharacter($character: UpdateCharacterInput!) { - updateCharacter(input: $character) { - character { - name - } - } - }`, + updateCharacter(input: $character) { + character { + name + } + } + }`, Variables: map[string]interface{}{"character": map[string]interface{}{ "filter": map[string]interface{}{ "id": []string{id}, @@ -2589,49 +2590,49 @@ func queryInterfaceAfterAddMutation(t *testing.T) { t.Run("test query all characters", func(t *testing.T) { queryCharacterParams := &GraphQLParams{ Query: `query { - queryCharacter { - id - name - appearsIn - ... on Human { - starships { - name - length - } - totalCredits - } - ... on Droid { - primaryFunction - } - } - }`, + queryCharacter { + id + name + appearsIn + ... on Human { + starships { + name + length + } + totalCredits + } + ... on Droid { + primaryFunction + } + } + }`, } gqlResponse := queryCharacterParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := fmt.Sprintf(`{ - "queryCharacter": [ - { - "id": "%s", - "name": "Han Solo", - "appearsIn": ["EMPIRE"], - "starships": [ - { - "name": "Millennium Falcon", - "length": 2 - } - ], - "totalCredits": 10 - }, - { - "id": "%s", - "name": "R2-D2", - "appearsIn": ["EMPIRE"], - "primaryFunction": "Robot" - } - ] - }`, humanID, droidID) + "queryCharacter": [ + { + "id": "%s", + "name": "Han Solo", + "appearsIn": ["EMPIRE"], + "starships": [ + { + "name": "Millennium Falcon", + "length": 2 + } + ], + "totalCredits": 10 + }, + { + "id": "%s", + "name": "R2-D2", + "appearsIn": ["EMPIRE"], + "primaryFunction": "Robot" + } + ] + }`, humanID, droidID) testutil.CompareJSON(t, expected, string(gqlResponse.Data)) }) @@ -2639,119 +2640,119 @@ func queryInterfaceAfterAddMutation(t *testing.T) { t.Run("test query characters by name", func(t *testing.T) { queryCharacterByNameParams := &GraphQLParams{ Query: `query { - queryCharacter(filter: { name: { eq: "Han Solo" } }) { - id - name - appearsIn - ... on Human { - starships { - name - length - } - totalCredits - } - ... on Droid { - primaryFunction - } - } - }`, + queryCharacter(filter: { name: { eq: "Han Solo" } }) { + id + name + appearsIn + ... on Human { + starships { + name + length + } + totalCredits + } + ... on Droid { + primaryFunction + } + } + }`, } gqlResponse := queryCharacterByNameParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := fmt.Sprintf(`{ - "queryCharacter": [ - { - "id": "%s", - "name": "Han Solo", - "appearsIn": ["EMPIRE"], - "starships": [ - { - "name": "Millennium Falcon", - "length": 2 - } - ], - "totalCredits": 10 - } - ] - }`, humanID) + "queryCharacter": [ + { + "id": "%s", + "name": "Han Solo", + "appearsIn": ["EMPIRE"], + "starships": [ + { + "name": "Millennium Falcon", + "length": 2 + } + ], + "totalCredits": 10 + } + ] + }`, humanID) testutil.CompareJSON(t, expected, string(gqlResponse.Data)) }) t.Run("test query all humans", func(t *testing.T) { queryHumanParams := &GraphQLParams{ Query: `query { - queryHuman { - id - name - appearsIn - starships { - name - length - } - totalCredits - } - }`, + queryHuman { + id + name + appearsIn + starships { + name + length + } + totalCredits + } + }`, } gqlResponse := queryHumanParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := fmt.Sprintf(`{ - "queryHuman": [ - { - "id": "%s", - "name": "Han Solo", - "appearsIn": ["EMPIRE"], - "starships": [ - { - "name": "Millennium Falcon", - "length": 2 - } - ], - "totalCredits": 10 - } - ] - }`, humanID) + "queryHuman": [ + { + "id": "%s", + "name": "Han Solo", + "appearsIn": ["EMPIRE"], + "starships": [ + { + "name": "Millennium Falcon", + "length": 2 + } + ], + "totalCredits": 10 + } + ] + }`, humanID) testutil.CompareJSON(t, expected, string(gqlResponse.Data)) }) t.Run("test query humans by name", func(t *testing.T) { queryHumanParamsByName := &GraphQLParams{ Query: `query { - queryHuman(filter: { name: { eq: "Han Solo" } }) { - id - name - appearsIn - starships { - name - length - } - totalCredits - } - }`, + queryHuman(filter: { name: { eq: "Han Solo" } }) { + id + name + appearsIn + starships { + name + length + } + totalCredits + } + }`, } gqlResponse := queryHumanParamsByName.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) expected := fmt.Sprintf(`{ - "queryHuman": [ - { - "id": "%s", - "name": "Han Solo", - "appearsIn": ["EMPIRE"], - "starships": [ - { - "name": "Millennium Falcon", - "length": 2 - } - ], - "totalCredits": 10 - } - ] - }`, humanID) + "queryHuman": [ + { + "id": "%s", + "name": "Han Solo", + "appearsIn": ["EMPIRE"], + "starships": [ + { + "name": "Millennium Falcon", + "length": 2 + } + ], + "totalCredits": 10 + } + ] + }`, humanID) testutil.CompareJSON(t, expected, string(gqlResponse.Data)) }) @@ -2780,16 +2781,16 @@ func requireState(t *testing.T, uid string, expectedState *state, params := &GraphQLParams{ Query: `query getState($id: ID!) { - getState(id: $id) { - id - xcode - name - country { - id - name - } - } - }`, + getState(id: $id) { + id + xcode + name + country { + id + name + } + } + }`, Variables: map[string]interface{}{"id": uid}, } gqlResponse := executeRequest(t, GraphqlURL, params) @@ -2809,18 +2810,18 @@ func requireState(t *testing.T, uid string, expectedState *state, func addState(t *testing.T, name string, executeRequest requestExecutor) *state { addStateParams := &GraphQLParams{ Query: `mutation addState($xcode: String!, $name: String!) { - addState(input: [{ xcode: $xcode, name: $name }]) { - state { - id - xcode - name - } - } - }`, + addState(input: [{ xcode: $xcode, name: $name }]) { + state { + id + xcode + name + } + } + }`, Variables: map[string]interface{}{"name": name, "xcode": "cal"}, } addStateExpected := ` - { "addState": { "state": [{ "id": "_UID_", "name": "` + name + `", "xcode": "cal" } ]} }` + { "addState": { "state": [{ "id": "_UID_", "name": "` + name + `", "xcode": "cal" } ]} }` gqlResponse := executeRequest(t, GraphqlURL, addStateParams) RequireNoGQLErrors(t, gqlResponse) @@ -2864,8 +2865,8 @@ func DeleteGqlType( deleteTypeParams := &GraphQLParams{ Query: fmt.Sprintf(`mutation delete%s($filter: %sFilter!) { - delete%s(filter: $filter) { msg numUids } - }`, typeName, typeName, typeName), + delete%s(filter: $filter) { msg numUids } + }`, typeName, typeName, typeName), Variables: map[string]interface{}{"filter": filter}, } @@ -2903,14 +2904,14 @@ func addMutationWithXid(t *testing.T, executeRequest requestExecutor) { name := "Calgary" addStateParams := &GraphQLParams{ Query: `mutation addState($xcode: String!, $name: String!) { - addState(input: [{ xcode: $xcode, name: $name }]) { - state { - id - xcode - name - } - } - }`, + addState(input: [{ xcode: $xcode, name: $name }]) { + state { + id + xcode + name + } + } + }`, Variables: map[string]interface{}{"name": name, "xcode": "cal"}, } @@ -2961,16 +2962,16 @@ func addMultipleMutationWithOneError(t *testing.T) { addPostParams := &GraphQLParams{ Query: `mutation addPost($posts: [AddPostInput!]!) { - addPost(input: $posts) { - post { - postID - title - author { - id - } - } - } - }`, + addPost(input: $posts) { + post { + postID + title + author { + id + } + } + } + }`, Variables: map[string]interface{}{"posts": []*post{goodPost, badPost, anotherGoodPost}}, } @@ -2978,18 +2979,18 @@ func addMultipleMutationWithOneError(t *testing.T) { gqlResponse := postExecutor(t, GraphqlURL, addPostParams) addPostExpected := fmt.Sprintf(`{ "addPost": { - "post": [{ - "title": "Text Post", - "author": { - "id": "%s" - } - }, { - "title": "Another Test Post", - "author": { - "id": "%s" - } - }] - } }`, newAuth.ID, newAuth.ID) + "post": [{ + "title": "Text Post", + "author": { + "id": "%s" + } + }, { + "title": "Another Test Post", + "author": { + "id": "%s" + } + }] + } }`, newAuth.ID, newAuth.ID) var expected, result struct { AddPost struct { @@ -3002,7 +3003,7 @@ func addMultipleMutationWithOneError(t *testing.T) { require.NoError(t, err) require.Contains(t, gqlResponse.Errors[0].Error(), - `couldn't rewrite query for mutation addPost because ID "0x0" isn't a Author`) + `because ID "0x0" isn't a Author`) cleanUp(t, []*country{newCountry}, []*author{newAuth}, result.AddPost.Post) } @@ -3010,20 +3011,20 @@ func addMultipleMutationWithOneError(t *testing.T) { func addMovie(t *testing.T, executeRequest requestExecutor) *movie { addMovieParams := &GraphQLParams{ Query: `mutation addMovie($name: String!) { - addMovie(input: [{ name: $name }]) { - movie { - id - name - director { - name - } - } - } - }`, + addMovie(input: [{ name: $name }]) { + movie { + id + name + director { + name + } + } + } + }`, Variables: map[string]interface{}{"name": "Testmovie"}, } addMovieExpected := ` - { "addMovie": { "movie": [{ "id": "_UID_", "name": "Testmovie", "director": [] }] } }` + { "addMovie": { "movie": [{ "id": "_UID_", "name": "Testmovie", "director": [] }] } }` gqlResponse := executeRequest(t, GraphqlURL, addMovieParams) RequireNoGQLErrors(t, gqlResponse) @@ -3055,9 +3056,9 @@ func cleanupMovieAndDirector(t *testing.T, movieID, directorID string) { // Delete everything multiMutationParams := &GraphQLParams{ Query: `mutation cleanup($movieFilter: MovieFilter!, $dirFilter: MovieDirectorFilter!) { - deleteMovie(filter: $movieFilter) { msg } - deleteMovieDirector(filter: $dirFilter) { msg } - }`, + deleteMovie(filter: $movieFilter) { msg } + deleteMovieDirector(filter: $dirFilter) { msg } + }`, Variables: map[string]interface{}{ "movieFilter": map[string]interface{}{ "id": []string{movieID}, @@ -3068,8 +3069,8 @@ func cleanupMovieAndDirector(t *testing.T, movieID, directorID string) { }, } multiMutationExpected := `{ - "deleteMovie": { "msg": "Deleted" }, - "deleteMovieDirector" : { "msg": "Deleted" } + "deleteMovie": { "msg": "Deleted" }, + "deleteMovieDirector" : { "msg": "Deleted" } }` gqlResponse := multiMutationParams.ExecuteAsPost(t, GraphqlURL) @@ -3087,13 +3088,13 @@ func addMutationWithReverseDgraphEdge(t *testing.T) { addMovieDirectorParams := &GraphQLParams{ Query: `mutation addMovieDirector($dir: [AddMovieDirectorInput!]!) { - addMovieDirector(input: $dir) { - movieDirector { - id - name - } - } - }`, + addMovieDirector(input: $dir) { + movieDirector { + id + name + } + } + }`, Variables: map[string]interface{}{"dir": []map[string]interface{}{{ "name": "Spielberg", "directed": []map[string]interface{}{{"id": newMovie.ID}}, @@ -3128,13 +3129,13 @@ func addMutationWithReverseDgraphEdge(t *testing.T) { getMovieParams := &GraphQLParams{ Query: `query getMovie($id: ID!) { - getMovie(id: $id) { - name - director { - name - } - } - }`, + getMovie(id: $id) { + name + director { + name + } + } + }`, Variables: map[string]interface{}{ "id": newMovie.ID, }, @@ -3171,16 +3172,16 @@ func testNumUids(t *testing.T) { addAuthorParams := &GraphQLParams{ Query: `mutation addAuthor($author: [AddAuthorInput!]!) { - addAuthor(input: $author) { - numUids - author { - id - posts { - postID - } - } - } - }`, + addAuthor(input: $author) { + numUids + author { + id + posts { + postID + } + } + } + }`, Variables: map[string]interface{}{"author": []*author{auth}}, } @@ -3203,10 +3204,10 @@ func testNumUids(t *testing.T) { t.Run("Test numUID in update", func(t *testing.T) { updatePostParams := &GraphQLParams{ Query: `mutation updatePosts($posts: UpdatePostInput!) { - updatePost(input: $posts) { - numUids - } - }`, + updatePost(input: $posts) { + numUids + } + }`, Variables: map[string]interface{}{"posts": map[string]interface{}{ "filter": map[string]interface{}{ "title": map[string]interface{}{ @@ -3237,17 +3238,17 @@ func testNumUids(t *testing.T) { t.Run("Test numUID in delete", func(t *testing.T) { deleteAuthorParams := &GraphQLParams{ Query: `mutation deleteItems($authorFilter: AuthorFilter!, - $postFilter: PostFilter!) { + $postFilter: PostFilter!) { - deleteAuthor(filter: $authorFilter) { - numUids - } + deleteAuthor(filter: $authorFilter) { + numUids + } - deletePost(filter: $postFilter) { - numUids - msg - } - }`, + deletePost(filter: $postFilter) { + numUids + msg + } + }`, Variables: map[string]interface{}{ "postFilter": map[string]interface{}{ "title": map[string]interface{}{ @@ -3310,55 +3311,357 @@ func threeLevelDeepMutation(t *testing.T) { addStudentParams := &GraphQLParams{ Query: `mutation addStudent($input: [AddStudentInput!]!) { - addStudent(input: $input) { - student { - xid - name - taughtBy { - id - xid - name - subject - teaches (order: {asc:xid}) { - xid - taughtBy { - name - xid - subject - } - } - } - } - } - }`, + addStudent(input: $input) { + student { + xid + name + taughtBy { + xid + name + subject + teaches (order: {asc:xid}) { + xid + taughtBy { + name + xid + subject + } + } + } + } + } + }`, Variables: map[string]interface{}{"input": newStudents}, } gqlResponse := postExecutor(t, GraphqlURL, addStudentParams) RequireNoGQLErrors(t, gqlResponse) - var actualResult struct { - AddStudent struct { - Student []*student + addStudentExpected := `{ + "addStudent": { + "student": [ + { + "xid": "HS1", + "name": "Stud1", + "taughtBy": [ + { + "xid": "HT0", + "name": "Teacher0", + "subject": "English", + "teaches": [ + { + "xid": "HS1", + "taughtBy": [ + { + "name": "Teacher0", + "xid": "HT0", + "subject": "English" + } + ] + }, + { + "xid": "HS2", + "taughtBy": [ + { + "name": "Teacher0", + "xid": "HT0", + "subject": "English" + } + ] + } + ] + } + ] + } + ] + } + }` + testutil.CompareJSON(t, addStudentExpected, string(gqlResponse.Data)) + + // cleanup + filter := getXidFilter("xid", []string{"HS1", "HS2"}) + DeleteGqlType(t, "Student", filter, 2, nil) + filter = getXidFilter("xid", []string{"HT0"}) + DeleteGqlType(t, "Teacher", filter, 1, nil) + +} + +func parallelMutations(t *testing.T) { + // Add 20 mutations simultaneously using go routine. + // Only one for each xcode should be added. + // Each goroutine adds num different new nodes. + executeMutation := func(wg *sync.WaitGroup, num int) { + defer wg.Done() + for i := 0; i < num; i++ { + addStateParams := &GraphQLParams{ + Query: fmt.Sprintf(`mutation { + addState(input: [{xcode: "NewS%d", name: "State%d"}]) { + state { + xcode + name + } + } + }`, i, i), + } + _ = addStateParams.ExecuteAsPost(t, GraphqlURL) } } - err := json.Unmarshal(gqlResponse.Data, &actualResult) - require.NoError(t, err) + var wg sync.WaitGroup + + // Nodes to be added per each goroutine + num := 5 + for i := 0; i < 20; i++ { + wg.Add(1) + go executeMutation(&wg, num) + } + wg.Wait() + + for i := 0; i < num; i++ { + getStateParams := &GraphQLParams{ + Query: fmt.Sprintf(`query { + queryState(filter: { xcode: { eq: "NewS%d"}}) { + name + } + }`, i), + } + + // As we are using the same XID in all mutations. Only one should succeed. + gqlResponse := getStateParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + require.Equal(t, fmt.Sprintf(`{"queryState":[{"name":"State%d"}]}`, i), string(gqlResponse.Data)) + + filter := map[string]interface{}{"xcode": map[string]interface{}{"eq": fmt.Sprintf("NewS%d", i)}} + deleteState(t, filter, 1, nil) + } +} + +func cyclicMutation(t *testing.T) { + // Student HS1 -->taught by --> Teacher T0 --> teaches --> Student HS2 --> taught by --> Teacher T1 --> teaches --> Student HS1 + newStudent := &student{ + Xid: "HS1", + Name: "Stud1", + TaughtBy: []*teacher{ + { + Xid: "HT0", + Name: "Teacher0", + Teaches: []*student{{ + Xid: "HS2", + Name: "Stud2", + TaughtBy: []*teacher{ + { + Xid: "HT1", + Name: "Teacher1", + Teaches: []*student{{ + Xid: "HS1", + }}, + }, + }, + }}, + }, + }, + } + + newStudents := []*student{newStudent} + + addStudentParams := &GraphQLParams{ + Query: `mutation addStudent($input: [AddStudentInput!]!) { + addStudent(input: $input) { + student { + xid + name + taughtBy (order: {asc:xid}) { + xid + name + teaches (order: {asc:xid}) { + xid + name + taughtBy (order:{asc:xid}) { + name + xid + teaches (order:{asc:xid}) { + xid + name + } + } + } + } + } + } + }`, + Variables: map[string]interface{}{"input": newStudents}, + } - require.Equal(t, actualResult.AddStudent.Student[0].Xid, "HS1") - require.Equal(t, actualResult.AddStudent.Student[0].TaughtBy[0].Xid, "HT0") - require.Equal(t, actualResult.AddStudent.Student[0].TaughtBy[0].Teaches[0].Xid, "HS1") - require.Equal(t, actualResult.AddStudent.Student[0].TaughtBy[0].Teaches[0].TaughtBy[0].Xid, "HT0") - require.Equal(t, actualResult.AddStudent.Student[0].TaughtBy[0].Teaches[1].Xid, "HS2") - require.Equal(t, actualResult.AddStudent.Student[0].TaughtBy[0].Teaches[1].TaughtBy[0].Xid, "HT0") + gqlResponse := postExecutor(t, GraphqlURL, addStudentParams) + RequireNoGQLErrors(t, gqlResponse) + + addStudentExpected := `{ + "addStudent": { + "student": [ + { + "xid": "HS1", + "name": "Stud1", + "taughtBy": [ + { + "xid": "HT0", + "name": "Teacher0", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1", + "taughtBy": [ + { + "name": "Teacher0", + "xid": "HT0", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1" + }, + { + "xid": "HS2", + "name": "Stud2" + } + ] + }, + { + "name": "Teacher1", + "xid": "HT1", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1" + }, + { + "xid": "HS2", + "name": "Stud2" + } + ] + } + ] + }, + { + "xid": "HS2", + "name": "Stud2", + "taughtBy": [ + { + "name": "Teacher0", + "xid": "HT0", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1" + }, + { + "xid": "HS2", + "name": "Stud2" + } + ] + }, + { + "name": "Teacher1", + "xid": "HT1", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1" + }, + { + "xid": "HS2", + "name": "Stud2" + } + ] + } + ] + } + ] + }, + { + "xid": "HT1", + "name": "Teacher1", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1", + "taughtBy": [ + { + "name": "Teacher0", + "xid": "HT0", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1" + }, + { + "xid": "HS2", + "name": "Stud2" + } + ] + }, + { + "name": "Teacher1", + "xid": "HT1", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1" + }, + { + "xid": "HS2", + "name": "Stud2" + } + ] + } + ] + }, + { + "xid": "HS2", + "name": "Stud2", + "taughtBy": [ + { + "name": "Teacher0", + "xid": "HT0", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1" + }, + { + "xid": "HS2", + "name": "Stud2" + } + ] + }, + { + "name": "Teacher1", + "xid": "HT1", + "teaches": [ + { + "xid": "HS1", + "name": "Stud1" + }, + { + "xid": "HS2", + "name": "Stud2" + } + ] + } + ] + } + ] + } + ] + } + ] + } + }` + testutil.CompareJSON(t, addStudentExpected, string(gqlResponse.Data)) // cleanup filter := getXidFilter("xid", []string{"HS1", "HS2"}) DeleteGqlType(t, "Student", filter, 2, nil) - filter = getXidFilter("xid", []string{"HT0"}) - DeleteGqlType(t, "Teacher", filter, 1, nil) - + filter = getXidFilter("xid", []string{"HT0", "HT1"}) + DeleteGqlType(t, "Teacher", filter, 2, nil) } func deepMutationDuplicateXIDsSameObjectTest(t *testing.T) { @@ -3394,19 +3697,19 @@ func deepMutationDuplicateXIDsSameObjectTest(t *testing.T) { addStudentParams := &GraphQLParams{ Query: `mutation addStudent($input: [AddStudentInput!]!) { - addStudent(input: $input) { - student { - xid - name - taughtBy { - id - xid - name - subject - } - } - } - }`, + addStudent(input: $input) { + student { + xid + name + taughtBy { + id + xid + name + subject + } + } + } + }`, Variables: map[string]interface{}{"input": newStudents}, } @@ -3487,33 +3790,33 @@ func queryTypenameInMutation(t *testing.T) { addStateParams := &GraphQLParams{ Query: `mutation { __typename - a:__typename - addState(input: [{xcode: "S1", name: "State1"}]) { - state { - __typename - xcode - name - } - __typename - } - }`, + a:__typename + addState(input: [{xcode: "S1", name: "State1"}]) { + state { + __typename + xcode + name + } + __typename + } + }`, } gqlResponse := addStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) addStateExpected := `{ - "__typename":"Mutation", - "a":"Mutation", - "addState": { - "state": [{ - "__typename": "State", - "xcode": "S1", - "name": "State1" - }], - "__typename": "AddStatePayload" - } - }` + "__typename":"Mutation", + "a":"Mutation", + "addState": { + "state": [{ + "__typename": "State", + "xcode": "S1", + "name": "State1" + }], + "__typename": "AddStatePayload" + } + }` testutil.CompareJSON(t, addStateExpected, string(gqlResponse.Data)) filter := map[string]interface{}{"xcode": map[string]interface{}{"eq": "S1"}} @@ -3524,28 +3827,28 @@ func ensureAliasInMutationPayload(t *testing.T) { // querying __typename, numUids and state with alias addStateParams := &GraphQLParams{ Query: `mutation { - addState(input: [{xcode: "S1", name: "State1"}]) { - type: __typename - numUids - count: numUids - op: state { - xcode - } - } - }`, + addState(input: [{xcode: "S1", name: "State1"}]) { + type: __typename + numUids + count: numUids + op: state { + xcode + } + } + }`, } gqlResponse := addStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) addStateExpected := `{ - "addState": { - "type": "AddStatePayload", - "numUids": 1, - "count": 1, - "op": [{"xcode":"S1"}] - } - }` + "addState": { + "type": "AddStatePayload", + "numUids": 1, + "count": 1, + "op": [{"xcode":"S1"}] + } + }` require.JSONEq(t, addStateExpected, string(gqlResponse.Data)) filter := map[string]interface{}{"xcode": map[string]interface{}{"eq": "S1"}} @@ -3555,12 +3858,12 @@ func ensureAliasInMutationPayload(t *testing.T) { func mutationsHaveExtensions(t *testing.T) { mutation := &GraphQLParams{ Query: `mutation { - addCategory(input: [{ name: "cat" }]) { - category { - id - } - } - }`, + addCategory(input: [{ name: "cat" }]) { + category { + id + } + } + }`, } touchedUidskey := "touched_uids" @@ -3586,28 +3889,28 @@ func mutationsWithAlias(t *testing.T) { aliasMutationParams := &GraphQLParams{ Query: `mutation alias($filter: CountryFilter!) { - upd: updateCountry(input: { - filter: $filter - set: { name: "Testland Alias" } - }) { - updatedCountry: country { - name - theName: name - } - } - - del: deleteCountry(filter: $filter) { - message: msg - uids: numUids - } - }`, + upd: updateCountry(input: { + filter: $filter + set: { name: "Testland Alias" } + }) { + updatedCountry: country { + name + theName: name + } + } + + del: deleteCountry(filter: $filter) { + message: msg + uids: numUids + } + }`, Variables: map[string]interface{}{ "filter": map[string]interface{}{"id": []string{newCountry.ID}}}, } multiMutationExpected := `{ - "upd": { "updatedCountry": [{ "name": "Testland Alias", "theName": "Testland Alias" }] }, - "del" : { "message": "Deleted", "uids": 1 } - }` + "upd": { "updatedCountry": [{ "name": "Testland Alias", "theName": "Testland Alias" }] }, + "del" : { "message": "Deleted", "uids": 1 } + }` gqlResponse := aliasMutationParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) @@ -3620,25 +3923,25 @@ func updateMutationWithoutSetRemove(t *testing.T) { updateCountryParams := &GraphQLParams{ Query: `mutation updateCountry($id: ID!){ - updateCountry(input: {filter: {id: [$id]}}) { - numUids - country { - id - name - } - } - }`, + updateCountry(input: {filter: {id: [$id]}}) { + numUids + country { + id + name + } + } + }`, Variables: map[string]interface{}{"id": country.ID}, } gqlResponse := updateCountryParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, `{ - "updateCountry": { - "numUids": 0, - "country": [] - } - }`, string(gqlResponse.Data)) + "updateCountry": { + "numUids": 0, + "country": [] + } + }`, string(gqlResponse.Data)) // cleanup deleteCountry(t, map[string]interface{}{"id": []string{country.ID}}, 1, nil) @@ -3647,26 +3950,26 @@ func updateMutationWithoutSetRemove(t *testing.T) { func checkCascadeWithMutationWithoutIDField(t *testing.T) { addStateParams := &GraphQLParams{ Query: `mutation { - addState(input: [{xcode: "S2", name: "State2"}]) @cascade(fields:["numUids"]) { - state @cascade(fields:["xcode"]) { - xcode - name - } - } - }`, + addState(input: [{xcode: "S2", name: "State2"}]) @cascade(fields:["numUids"]) { + state @cascade(fields:["xcode"]) { + xcode + name + } + } + }`, } gqlResponse := addStateParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) addStateExpected := `{ - "addState": { - "state": [{ - "xcode": "S2", - "name": "State2" - }] - } - }` + "addState": { + "state": [{ + "xcode": "S2", + "name": "State2" + }] + } + }` testutil.CompareJSON(t, addStateExpected, string(gqlResponse.Data)) filter := map[string]interface{}{"xcode": map[string]interface{}{"eq": "S2"}} @@ -3678,30 +3981,30 @@ func int64BoundaryTesting(t *testing.T) { //(2^63)=9223372036854775808 addPost1Params := &GraphQLParams{ Query: `mutation { - addpost1(input: [{title: "Dgraph", numLikes: 9223372036854775807 },{title: "Dgraph1", numLikes: -9223372036854775808 }]) { - post1 { - title - numLikes - } - } - }`, + 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 - }] - } - }` + "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) @@ -3716,45 +4019,45 @@ func intWithList(t *testing.T) { }{{ name: "list of integers in mutation", query: `mutation { - addpost1(input: [{title: "Dgraph",commentsByMonth:[2,33,11,6],likesByMonth:[4,33,1,66] }]) { - post1 { - title + addpost1(input: [{title: "Dgraph",commentsByMonth:[2,33,11,6],likesByMonth:[4,33,1,66] }]) { + post1 { + title commentsByMonth likesByMonth - } - } - }`, + } + } + }`, expected: `{ - "addpost1": { - "post1": [{ - "title": "Dgraph", - "commentsByMonth": [2,33,11,6], + "addpost1": { + "post1": [{ + "title": "Dgraph", + "commentsByMonth": [2,33,11,6], "likesByMonth": [4,33,1,66] - }] - } - }`, + }] + } + }`, }, { name: "list of integers in variable", query: `mutation($post1:[Addpost1Input!]!) { - addpost1(input:$post1 ) { - post1 { - title + addpost1(input:$post1 ) { + post1 { + title commentsByMonth likesByMonth - } - } - }`, + } + } + }`, variables: map[string]interface{}{"post1": []interface{}{map[string]interface{}{"title": "Dgraph", "commentsByMonth": []int{2, 33, 11, 6}, "likesByMonth": []int64{4, 33, 1, 66}}}}, expected: `{ - "addpost1": { - "post1": [{ - "title": "Dgraph", - "commentsByMonth": [2,33,11,6], + "addpost1": { + "post1": [{ + "title": "Dgraph", + "commentsByMonth": [2,33,11,6], "likesByMonth": [4,33,1,66] - }] - } - }`, + }] + } + }`, }} for _, tcase := range tcases { t.Run(tcase.name, func(t *testing.T) { @@ -3775,18 +4078,18 @@ func intWithList(t *testing.T) { func nestedAddMutationWithHasInverse(t *testing.T) { params := &GraphQLParams{ Query: `mutation addPerson1($input: [AddPerson1Input!]!) { - addPerson1(input: $input) { - person1 { - name - friends { - name - friends { - name - } - } - } - } - }`, + addPerson1(input: $input) { + person1 { + name + friends { + name + friends { + name + } + } + } + } + }`, Variables: map[string]interface{}{ "input": []interface{}{ map[string]interface{}{ @@ -3810,27 +4113,27 @@ func nestedAddMutationWithHasInverse(t *testing.T) { RequireNoGQLErrors(t, gqlResponse) expected := `{ - "addPerson1": { - "person1": [ - { - "friends": [ - { - "friends": [ - { - "name": "Or" - }, - { - "name": "Justin" - } - ], - "name": "Michal" - } - ], - "name": "Or" - } - ] - } - }` + "addPerson1": { + "person1": [ + { + "friends": [ + { + "friends": [ + { + "name": "Or" + }, + { + "name": "Justin" + } + ], + "name": "Michal" + } + ], + "name": "Or" + } + ] + } + }` testutil.CompareJSON(t, expected, string(gqlResponse.Data)) // cleanup @@ -3840,18 +4143,18 @@ func nestedAddMutationWithHasInverse(t *testing.T) { func mutationPointType(t *testing.T) { addHotelParams := &GraphQLParams{ Query: ` - mutation addHotel($hotel: AddHotelInput!) { - addHotel(input: [$hotel]) { - hotel { - name - location { - __typename - latitude - longitude - } - } - } - }`, + mutation addHotel($hotel: AddHotelInput!) { + addHotel(input: [$hotel]) { + hotel { + name + location { + __typename + latitude + longitude + } + } + } + }`, Variables: map[string]interface{}{"hotel": map[string]interface{}{ "name": "Taj Hotel", "location": map[string]interface{}{ @@ -3864,18 +4167,18 @@ func mutationPointType(t *testing.T) { RequireNoGQLErrors(t, gqlResponse) addHotelExpected := ` - { - "addHotel": { - "hotel": [{ - "name": "Taj Hotel", - "location": { - "__typename": "Point", - "latitude": 11.11, - "longitude": 22.22 - } - }] - } - }` + { + "addHotel": { + "hotel": [{ + "name": "Taj Hotel", + "location": { + "__typename": "Point", + "latitude": 11.11, + "longitude": 22.22 + } + }] + } + }` testutil.CompareJSON(t, addHotelExpected, string(gqlResponse.Data)) // Cleanup @@ -3885,114 +4188,114 @@ func mutationPointType(t *testing.T) { func mutationPolygonType(t *testing.T) { addHotelParams := &GraphQLParams{ Query: ` - mutation addHotel { - addHotel(input: [ - { - name: "Taj Hotel" - area : { - coordinates: [{ - points: [{ - latitude: 11.11, - longitude: 22.22 - }, { - latitude: 15.15, - longitude: 16.16 - }, { - latitude: 20.20, - longitude: 21.21 - }, - { - latitude: 11.11, - longitude: 22.22 - }] - }, { - points: [{ - latitude: 11.18, - longitude: 22.28 - }, { - latitude: 15.18, - longitude: 16.18 - }, { - latitude: 20.28, - longitude: 21.28 - }, { - latitude: 11.18, - longitude: 22.28 - }] - }] - } - } - ]) { - hotel { - name - area { - __typename - coordinates { + mutation addHotel { + addHotel(input: [ + { + name: "Taj Hotel" + area : { + coordinates: [{ + points: [{ + latitude: 11.11, + longitude: 22.22 + }, { + latitude: 15.15, + longitude: 16.16 + }, { + latitude: 20.20, + longitude: 21.21 + }, + { + latitude: 11.11, + longitude: 22.22 + }] + }, { + points: [{ + latitude: 11.18, + longitude: 22.28 + }, { + latitude: 15.18, + longitude: 16.18 + }, { + latitude: 20.28, + longitude: 21.28 + }, { + latitude: 11.18, + longitude: 22.28 + }] + }] + } + } + ]) { + hotel { + name + area { + __typename + coordinates { __typename - points { - latitude - __typename - longitude - } - } - } - } - } - }`, + points { + latitude + __typename + longitude + } + } + } + } + } + }`, } gqlResponse := addHotelParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) addHotelExpected := ` - { - "addHotel": { - "hotel": [{ - "name": "Taj Hotel", - "area": { - "__typename": "Polygon", - "coordinates": [{ - "__typename": "PointList", - "points": [{ - "__typename": "Point", - "latitude": 11.11, - "longitude": 22.22 - }, { - "__typename": "Point", - "latitude": 15.15, - "longitude": 16.16 - }, { - "__typename": "Point", - "latitude": 20.20, - "longitude": 21.21 - },{ - "__typename": "Point", - "latitude": 11.11, - "longitude": 22.22 - }] - }, { - "__typename": "PointList", - "points": [{ - "__typename": "Point", - "latitude": 11.18, - "longitude": 22.28 - }, { - "__typename": "Point", - "latitude": 15.18, - "longitude": 16.18 - }, { - "__typename": "Point", - "latitude": 20.28, - "longitude": 21.28 - }, { - "__typename": "Point", - "latitude": 11.18, - "longitude": 22.28 - }] - }] - } - }] - } - }` + { + "addHotel": { + "hotel": [{ + "name": "Taj Hotel", + "area": { + "__typename": "Polygon", + "coordinates": [{ + "__typename": "PointList", + "points": [{ + "__typename": "Point", + "latitude": 11.11, + "longitude": 22.22 + }, { + "__typename": "Point", + "latitude": 15.15, + "longitude": 16.16 + }, { + "__typename": "Point", + "latitude": 20.20, + "longitude": 21.21 + },{ + "__typename": "Point", + "latitude": 11.11, + "longitude": 22.22 + }] + }, { + "__typename": "PointList", + "points": [{ + "__typename": "Point", + "latitude": 11.18, + "longitude": 22.28 + }, { + "__typename": "Point", + "latitude": 15.18, + "longitude": 16.18 + }, { + "__typename": "Point", + "latitude": 20.28, + "longitude": 21.28 + }, { + "__typename": "Point", + "latitude": 11.18, + "longitude": 22.28 + }] + }] + } + }] + } + }` testutil.CompareJSON(t, addHotelExpected, string(gqlResponse.Data)) // Cleanup @@ -4002,190 +4305,190 @@ func mutationPolygonType(t *testing.T) { func mutationMultiPolygonType(t *testing.T) { addHotelParams := &GraphQLParams{ Query: ` - mutation addHotel { - addHotel(input: [{ - name: "Taj Hotel" - branches : { - polygons: [{ - coordinates: [{ - points: [{ - latitude: 11.11, - longitude: 22.22 - }, { - latitude: 15.15, - longitude: 16.16 - }, { - latitude: 20.20, - longitude: 21.21 - }, { - latitude: 11.11, - longitude: 22.22 - }] - }, { - points: [{ - latitude: 11.18, - longitude: 22.28 - }, { - latitude: 15.18, - longitude: 16.18 - }, { - latitude: 20.28, - longitude: 21.28 - }, { - latitude: 11.18, - longitude: 22.28 - }] - }] - }, { - coordinates: [{ - points: [{ - latitude: 91.11, - longitude: 92.22 - }, { - latitude: 15.15, - longitude: 16.16 - }, { - latitude: 20.20, - longitude: 21.21 - }, { - latitude: 91.11, - longitude: 92.22 - }] - }, { - points: [{ - latitude: 11.18, - longitude: 22.28 - }, { - latitude: 15.18, - longitude: 16.18 - }, { - latitude: 20.28, - longitude: 21.28 - }, { - latitude: 11.18, - longitude: 22.28 - }] - }] - }] - } - }]) { - hotel { - name - branches { - __typename - polygons { - __typename - coordinates { - __typename - points { - latitude - __typename - longitude - } - } - } - } - } - } - }`, + mutation addHotel { + addHotel(input: [{ + name: "Taj Hotel" + branches : { + polygons: [{ + coordinates: [{ + points: [{ + latitude: 11.11, + longitude: 22.22 + }, { + latitude: 15.15, + longitude: 16.16 + }, { + latitude: 20.20, + longitude: 21.21 + }, { + latitude: 11.11, + longitude: 22.22 + }] + }, { + points: [{ + latitude: 11.18, + longitude: 22.28 + }, { + latitude: 15.18, + longitude: 16.18 + }, { + latitude: 20.28, + longitude: 21.28 + }, { + latitude: 11.18, + longitude: 22.28 + }] + }] + }, { + coordinates: [{ + points: [{ + latitude: 91.11, + longitude: 92.22 + }, { + latitude: 15.15, + longitude: 16.16 + }, { + latitude: 20.20, + longitude: 21.21 + }, { + latitude: 91.11, + longitude: 92.22 + }] + }, { + points: [{ + latitude: 11.18, + longitude: 22.28 + }, { + latitude: 15.18, + longitude: 16.18 + }, { + latitude: 20.28, + longitude: 21.28 + }, { + latitude: 11.18, + longitude: 22.28 + }] + }] + }] + } + }]) { + hotel { + name + branches { + __typename + polygons { + __typename + coordinates { + __typename + points { + latitude + __typename + longitude + } + } + } + } + } + } + }`, } gqlResponse := addHotelParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) addHotelExpected := ` - { - "addHotel": { - "hotel": [{ - "name": "Taj Hotel", - "branches": { - "__typename": "MultiPolygon", - "polygons": [{ - "__typename": "Polygon", - "coordinates": [{ - "__typename": "PointList", - "points": [{ - "__typename": "Point", - "latitude": 11.11, - "longitude": 22.22 - }, { - "__typename": "Point", - "latitude": 15.15, - "longitude": 16.16 - }, { - "__typename": "Point", - "latitude": 20.20, - "longitude": 21.21 - },{ - "__typename": "Point", - "latitude": 11.11, - "longitude": 22.22 - }] - }, { - "__typename": "PointList", - "points": [{ - "__typename": "Point", - "latitude": 11.18, - "longitude": 22.28 - }, { - "__typename": "Point", - "latitude": 15.18, - "longitude": 16.18 - }, { - "__typename": "Point", - "latitude": 20.28, - "longitude": 21.28 - }, { - "__typename": "Point", - "latitude": 11.18, - "longitude": 22.28 - }] - }] - }, { - "__typename": "Polygon", - "coordinates": [{ - "__typename": "PointList", - "points": [{ - "__typename": "Point", - "latitude": 91.11, - "longitude": 92.22 - }, { - "__typename": "Point", - "latitude": 15.15, - "longitude": 16.16 - }, { - "__typename": "Point", - "latitude": 20.20, - "longitude": 21.21 - },{ - "__typename": "Point", - "latitude": 91.11, - "longitude": 92.22 - }] - }, { - "__typename": "PointList", - "points": [{ - "__typename": "Point", - "latitude": 11.18, - "longitude": 22.28 - }, { - "__typename": "Point", - "latitude": 15.18, - "longitude": 16.18 - }, { - "__typename": "Point", - "latitude": 20.28, - "longitude": 21.28 - }, { - "__typename": "Point", - "latitude": 11.18, - "longitude": 22.28 - }] - }] - }] - } - }] - } - }` + { + "addHotel": { + "hotel": [{ + "name": "Taj Hotel", + "branches": { + "__typename": "MultiPolygon", + "polygons": [{ + "__typename": "Polygon", + "coordinates": [{ + "__typename": "PointList", + "points": [{ + "__typename": "Point", + "latitude": 11.11, + "longitude": 22.22 + }, { + "__typename": "Point", + "latitude": 15.15, + "longitude": 16.16 + }, { + "__typename": "Point", + "latitude": 20.20, + "longitude": 21.21 + },{ + "__typename": "Point", + "latitude": 11.11, + "longitude": 22.22 + }] + }, { + "__typename": "PointList", + "points": [{ + "__typename": "Point", + "latitude": 11.18, + "longitude": 22.28 + }, { + "__typename": "Point", + "latitude": 15.18, + "longitude": 16.18 + }, { + "__typename": "Point", + "latitude": 20.28, + "longitude": 21.28 + }, { + "__typename": "Point", + "latitude": 11.18, + "longitude": 22.28 + }] + }] + }, { + "__typename": "Polygon", + "coordinates": [{ + "__typename": "PointList", + "points": [{ + "__typename": "Point", + "latitude": 91.11, + "longitude": 92.22 + }, { + "__typename": "Point", + "latitude": 15.15, + "longitude": 16.16 + }, { + "__typename": "Point", + "latitude": 20.20, + "longitude": 21.21 + },{ + "__typename": "Point", + "latitude": 91.11, + "longitude": 92.22 + }] + }, { + "__typename": "PointList", + "points": [{ + "__typename": "Point", + "latitude": 11.18, + "longitude": 22.28 + }, { + "__typename": "Point", + "latitude": 15.18, + "longitude": 16.18 + }, { + "__typename": "Point", + "latitude": 20.28, + "longitude": 21.28 + }, { + "__typename": "Point", + "latitude": 11.18, + "longitude": 22.28 + }] + }] + }] + } + }] + } + }` testutil.CompareJSON(t, addHotelExpected, string(gqlResponse.Data)) // Cleanup @@ -4195,19 +4498,19 @@ func mutationMultiPolygonType(t *testing.T) { func addMutationWithHasInverseOverridesCorrectly(t *testing.T) { params := &GraphQLParams{ Query: `mutation addCountry($input: [AddCountryInput!]!) { - addCountry(input: $input) { - country { - name - states{ - xcode - name - country{ - name - } - } - } - } - }`, + addCountry(input: $input) { + country { + name + states{ + xcode + name + country{ + name + } + } + } + } + }`, Variables: map[string]interface{}{ "input": []interface{}{ @@ -4235,30 +4538,30 @@ func addMutationWithHasInverseOverridesCorrectly(t *testing.T) { RequireNoGQLErrors(t, gqlResponse) expected := `{ - "addCountry": { - "country": [ - { - "name": "A country", - "states": [ - { - "country": { - "name": "A country" - }, - "name": "Alphabet", - "xcode": "abc" - }, - { - "country": { - "name": "A country" - }, - "name": "Vowel", - "xcode": "def" - } - ] - } - ] - } - }` + "addCountry": { + "country": [ + { + "name": "A country", + "states": [ + { + "country": { + "name": "A country" + }, + "name": "Alphabet", + "xcode": "abc" + }, + { + "country": { + "name": "A country" + }, + "name": "Vowel", + "xcode": "def" + } + ] + } + ] + } + }` testutil.CompareJSON(t, expected, string(gqlResponse.Data)) filter := map[string]interface{}{"name": map[string]interface{}{"eq": "A country"}} deleteCountry(t, filter, 1, nil) @@ -4271,13 +4574,13 @@ func addMutationWithHasInverseOverridesCorrectly(t *testing.T) { func addUniversity(t *testing.T) string { addUniversityParams := &GraphQLParams{ Query: `mutation addUniversity($university: AddUniversityInput!) { - addUniversity(input: [$university]) { - university { - id - name - } - } - }`, + addUniversity(input: [$university]) { + university { + id + name + } + } + }`, Variables: map[string]interface{}{"university": map[string]interface{}{ "name": "The Great University", }}, @@ -4304,13 +4607,13 @@ func addUniversity(t *testing.T) string { func updateUniversity(t *testing.T, id string) { updateUniversityParams := &GraphQLParams{ Query: `mutation updateUniversity($university: UpdateUniversityInput!) { - updateUniversity(input: $university) { - university { - name - numStudents - } - } - }`, + updateUniversity(input: $university) { + university { + name + numStudents + } + } + }`, Variables: map[string]interface{}{"university": map[string]interface{}{ "filter": map[string]interface{}{ "id": []string{id}, @@ -4346,131 +4649,131 @@ func filterInMutationsWithArrayForAndOr(t *testing.T) { { name: "Filter with OR at top level in Mutation", query: `mutation { - addpost1(input: [{title: "Dgraph", numLikes: 100}]) { - post1(filter:{or:{title:{eq: "Dgraph"}}}) { - title - numLikes - } - } - }`, + addpost1(input: [{title: "Dgraph", numLikes: 100}]) { + post1(filter:{or:{title:{eq: "Dgraph"}}}) { + title + numLikes + } + } + }`, expected: `{ - "addpost1": { - "post1": [ - { - "title": "Dgraph", - "numLikes": 100 - } - ] - } - }`, + "addpost1": { + "post1": [ + { + "title": "Dgraph", + "numLikes": 100 + } + ] + } + }`, }, { name: "Filter with OR at top level in Mutation using variables", query: `mutation($filter:post1Filter) { - addpost1(input: [{title: "Dgraph", numLikes: 100}]) { - post1(filter:$filter) { - title - numLikes - } - } - }`, + addpost1(input: [{title: "Dgraph", numLikes: 100}]) { + post1(filter:$filter) { + title + numLikes + } + } + }`, variables: `{"filter":{"or":{"title":{"eq": "Dgraph"}}}}`, expected: `{ - "addpost1": { - "post1": [ - { - "title": "Dgraph", - "numLikes": 100 - } - ] - } - }`, + "addpost1": { + "post1": [ + { + "title": "Dgraph", + "numLikes": 100 + } + ] + } + }`, }, { name: "Filter with AND at top level in Mutation", query: `mutation { - addpost1(input: [{title: "Dgraph", numLikes: 100}]) { - post1(filter:{and:{title:{eq: "Dgraph"}}}) { - title - numLikes - } - } - }`, + addpost1(input: [{title: "Dgraph", numLikes: 100}]) { + post1(filter:{and:{title:{eq: "Dgraph"}}}) { + title + numLikes + } + } + }`, expected: `{ - "addpost1": { - "post1": [ - { - "title": "Dgraph", - "numLikes": 100 - } - ] - } - }`, + "addpost1": { + "post1": [ + { + "title": "Dgraph", + "numLikes": 100 + } + ] + } + }`, }, { name: "Filter with AND at top level in Mutation using variables", query: `mutation($filter:post1Filter) { - addpost1(input: [{title: "Dgraph", numLikes: 100}]) { - post1(filter:$filter) { - title - numLikes - } - } - }`, + addpost1(input: [{title: "Dgraph", numLikes: 100}]) { + post1(filter:$filter) { + title + numLikes + } + } + }`, variables: `{"filter":{"and":{"title":{"eq": "Dgraph"}}}}`, expected: `{ - "addpost1": { - "post1": [ - { - "title": "Dgraph", - "numLikes": 100 - } - ] - } - }`, + "addpost1": { + "post1": [ + { + "title": "Dgraph", + "numLikes": 100 + } + ] + } + }`, }, { name: "Filter with Nested And-OR in Mutation", query: `mutation { - addpost1(input: [{title: "Dgraph", numLikes: 100}]) { - post1(filter:{and:[{title:{eq: "Dgraph"}},{or:{numLikes:{eq: 100}}}]}) { - title - numLikes - } - } - }`, + addpost1(input: [{title: "Dgraph", numLikes: 100}]) { + post1(filter:{and:[{title:{eq: "Dgraph"}},{or:{numLikes:{eq: 100}}}]}) { + title + numLikes + } + } + }`, expected: `{ - "addpost1": { - "post1": [ - { - "title": "Dgraph", - "numLikes": 100 - } - ] - } - }`, + "addpost1": { + "post1": [ + { + "title": "Dgraph", + "numLikes": 100 + } + ] + } + }`, }, { name: "Filter with Nested And-OR in Mutation using variables", query: `mutation($filter:post1Filter) { - addpost1(input: [{title: "Dgraph", numLikes: 100}]) { - post1(filter:$filter) { - title + addpost1(input: [{title: "Dgraph", numLikes: 100}]) { + post1(filter:$filter) { + title numLikes - } + } } - }`, + }`, variables: `{"filter": {"and": [{"title":{"eq": "Dgraph"}},{"or":{"numLikes":{"eq": 100}}}]}}`, expected: `{ - "addpost1": { - "post1": [ - { - "title": "Dgraph", - "numLikes": 100 - } - ] - } - }`, + "addpost1": { + "post1": [ + { + "title": "Dgraph", + "numLikes": 100 + } + ] + } + }`, }, } @@ -4498,13 +4801,13 @@ func filterInMutationsWithArrayForAndOr(t *testing.T) { func filterInUpdateMutationsWithFilterAndOr(t *testing.T) { params := &GraphQLParams{Query: `mutation { - addpost1(input: [{title: "Dgraph", numLikes: 100},{title: "Dgraph1", numLikes: 120}]) { - post1(filter:{title:{eq:"Dgraph"}}) { - title - numLikes - } - } - }`} + addpost1(input: [{title: "Dgraph", numLikes: 100},{title: "Dgraph1", numLikes: 120}]) { + post1(filter:{title:{eq:"Dgraph"}}) { + title + numLikes + } + } + }`} resp := params.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, resp) @@ -4516,47 +4819,47 @@ func filterInUpdateMutationsWithFilterAndOr(t *testing.T) { }{ {name: "Filter with Nested OR-AND in Update Mutation", query: `mutation updatepost1{ - updatepost1(input:{filter:{or:[{title:{eq:"Dgraph1"}},{and:{numLikes:{eq:130}}}]},set:{numLikes:200}}){ - post1{ - title - numLikes - } - } - }`, + updatepost1(input:{filter:{or:[{title:{eq:"Dgraph1"}},{and:{numLikes:{eq:130}}}]},set:{numLikes:200}}){ + post1{ + title + numLikes + } + } + }`, expected: `{ - "updatepost1": { - "post1": [ - { - "title": "Dgraph1", - "numLikes": 200 - } - ] - } - }`, + "updatepost1": { + "post1": [ + { + "title": "Dgraph1", + "numLikes": 200 + } + ] + } + }`, }, {name: "Filter with Nested OR-AND in Update Mutation using variables", query: `mutation updatepost1($post1:Updatepost1Input!) { - updatepost1(input:$post1){ - post1{ - title - numLikes - } - } - }`, + updatepost1(input:$post1){ + post1{ + title + numLikes + } + } + }`, variables: `{"post1": {"filter":{"or": [{"title":{"eq": "Dgraph1"}},{"and":{"numLikes":{"eq": 140}}}]}, - "set":{ - "numLikes": "200" - } - } - }`, + "set":{ + "numLikes": "200" + } + } + }`, expected: `{ - "updatepost1": { - "post1": [{ - "title": "Dgraph1", - "numLikes": 200 - }] - } - }`, + "updatepost1": { + "post1": [{ + "title": "Dgraph1", + "numLikes": 200 + }] + } + }`, }, } @@ -4580,3 +4883,188 @@ func filterInUpdateMutationsWithFilterAndOr(t *testing.T) { DeleteGqlType(t, "post1", filter, 2, nil) } + +func threeLevelDoubleXID(t *testing.T) { + // Query added to test if the bug https://discuss.dgraph.io/t/mutation-fails-because-of-error-some-variables-are-defined-twice/9487 + // has been fixed. + mutation := &GraphQLParams{ + Query: `mutation { + addCountry(input: [{ + name: "c1", + states: [{ + xcode: "s11", + name: "s11", + region: { + id: "r1", + name: "r1", + district: { + id: "d1", + name: "d1" + } + } + }] + }]) { + country { + id + name + states { + xcode + name + region { + id + name + district { + id + name + } + } + } + } + } + }`, + } + gqlResponse := mutation.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + var addCountryExpected = `{ + "addCountry": { + "country": [ + { + "name": "c1", + "states": [ + { + "xcode": "s11", + "name": "s11", + "region": { + "id": "r1", + "name": "r1", + "district": { + "id": "d1", + "name": "d1" + } + } + } + ] + } + ] + } + }` + + var result, expected struct { + AddCountry struct { + Country []*country + } + } + err := json.Unmarshal([]byte(gqlResponse.Data), &result) + require.NoError(t, err) + err = json.Unmarshal([]byte(addCountryExpected), &expected) + require.NoError(t, err) + + require.Equal(t, len(result.AddCountry.Country), 1) + countryID := result.AddCountry.Country[0].ID + requireUID(t, countryID) + + opt := cmpopts.IgnoreFields(country{}, "ID") + if diff := cmp.Diff(expected, result, opt); diff != "" { + t.Errorf("result mismatch (-want +got):\n%s", diff) + } + + // Clean Up + filter := map[string]interface{}{"id": []string{countryID}} + deleteCountry(t, filter, 1, nil) + filter = map[string]interface{}{"xcode": map[string]interface{}{"eq": "s11"}} + deleteState(t, filter, 1, nil) + DeleteGqlType(t, "Region", map[string]interface{}{}, 1, nil) + DeleteGqlType(t, "District", map[string]interface{}{}, 1, nil) +} + +func twoLevelsLinkedToXID(t *testing.T) { + // Query added to test if the bug https://discuss.dgraph.io/t/create-child-nodes-with-addparent/11311/5 + // has been fixed. + + // Add Owner + query := &GraphQLParams{ + Query: `mutation { + addOwner(input: [{username: "user", password: "password"}]) { + owner { + username + } + } + }`, + } + + response := query.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, response) + var expected = `{ + "addOwner": { + "owner": [{ + "username": "user" + }] + } + }` + require.JSONEq(t, expected, string(response.Data)) + + // Add dataset and project + query = &GraphQLParams{ + Query: `mutation { + addProject(input: + [ + { + id: "p1", + owner: { + username: "user" + }, + name: "project", + datasets: [{ + id: "d1", + owner: { + username: "user" + } + name: "dataset" + }] + } + ] + ) { + project { + id + owner { + username + } + name + datasets { + id + owner { + username + } + name + } + } + } + }`, + } + + response = query.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, response) + expected = `{ + "addProject": { + "project": [{ + "id": "p1", + "owner": { + "username": "user" + }, + "name": "project", + "datasets": [{ + "id": "d1", + "owner": { + "username": "user" + }, + "name": "dataset" + }] + }] + } + }` + require.JSONEq(t, expected, string(response.Data)) + DeleteGqlType(t, "Project", map[string]interface{}{}, 1, nil) + DeleteGqlType(t, "Owner", map[string]interface{}{}, 1, nil) + DeleteGqlType(t, "Dataset", map[string]interface{}{}, 1, nil) +} diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index 2f7f53928f7..0193b29474b 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -21,6 +21,7 @@ type State { xcode: String! @id @search(by: [regexp]) name: String! capital: String + region: Region country: Country @dgraph(pred: "inCountry") } @@ -283,3 +284,34 @@ type University @generate( name: String! numStudents: Int } + +type Region { + id: String! @id + name: String! + district: District +} + +type District { + id: String! @id + name: String! +} + +type Owner { + username: String! @id + password: String! + projects: [Project!] @hasInverse(field: owner) +} + +type Project { + id: String! @id + owner: Owner! + name: String! @search(by: [hash]) + datasets: [Dataset!] @hasInverse(field: project) +} + +type Dataset { + id: String! @id + owner: Owner! + project: Project! + name: String! @search(by: [hash]) +} diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index 319ceed416e..6e34af10389 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -1,981 +1,1161 @@ { - "schema": [ + "schema": [ + { + "predicate": "Animal.category", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ] + }, + { + "predicate": "Category.name", + "type": "string" + }, + { + "predicate": "Category.posts", + "type": "uid", + "list": true + }, + { + "predicate": "Cheetah.speed", + "type": "float" + }, + { + "predicate": "Comment1.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Comment1.replies", + "type": "uid", + "list": true + }, + { + "predicate": "Country.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ] + }, + { + "predicate": "Dataset.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Dataset.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ] + }, + { + "predicate": "Dataset.owner", + "type": "uid" + }, + { + "predicate": "Dataset.project", + "type": "uid" + }, + { + "predicate": "District.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "District.name", + "type": "string" + }, + { + "predicate": "Dog.breed", + "type": "string", + "index": true, + "tokenizer": [ + "term" + ] + }, + { + "predicate": "Home.address", + "type": "string" + }, + { + "predicate": "Home.favouriteMember", + "type": "uid" + }, + { + "predicate": "Home.members", + "type": "uid", + "list": true + }, + { + "predicate": "Hotel.area", + "type": "geo", + "index": true, + "tokenizer": [ + "geo" + ] + }, + { + "predicate": "Hotel.branches", + "type": "geo", + "index": true, + "tokenizer": [ + "geo" + ] + }, + { + "predicate": "Hotel.location", + "type": "geo", + "index": true, + "tokenizer": [ + "geo" + ] + }, + { + "predicate": "Hotel.name", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ] + }, + { + "predicate": "Human.starships", + "type": "uid", + "list": true + }, + { + "predicate": "Movie.name", + "type": "string" + }, + { + "predicate": "MovieDirector.name", + "type": "string" + }, + { + "predicate": "Owner.password", + "type": "string" + }, + { + "predicate": "Owner.projects", + "type": "uid", + "list": true + }, + { + "predicate": "Owner.username", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Parrot.repeatsWords", + "type": "string", + "list": true + }, + { + "predicate": "People.name", + "type": "string" + }, + { + "predicate": "People.xid", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Person1.friends", + "type": "uid", + "list": true + }, + { + "predicate": "Person1.name", + "type": "string" + }, + { + "predicate": "Plant.breed", + "type": "string" + }, + { + "predicate": "Post1.comments", + "type": "uid", + "list": true + }, + { + "predicate": "Post1.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Project.datasets", + "type": "uid", + "list": true + }, + { + "predicate": "Project.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Project.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ] + }, + { + "predicate": "Project.owner", + "type": "uid" + }, + { + "predicate": "Region.district", + "type": "uid" + }, + { + "predicate": "Region.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Region.name", + "type": "string" + }, + { + "predicate": "State.capital", + "type": "string" + }, + { + "predicate": "State.name", + "type": "string" + }, + { + "predicate": "State.region", + "type": "uid" + }, + { + "predicate": "State.xcode", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ], + "upsert": true + }, + { + "predicate": "Student.taughtBy", + "type": "uid", + "list": true + }, + { + "predicate": "Teacher.subject", + "type": "string" + }, + { + "predicate": "Teacher.teaches", + "type": "uid", + "list": true + }, + { + "predicate": "Thing.name", + "type": "string" + }, + { + "predicate": "ThingOne.color", + "type": "string" + }, + { + "predicate": "ThingOne.usedBy", + "type": "string" + }, + { + "predicate": "ThingTwo.color", + "type": "string" + }, + { + "predicate": "ThingTwo.owner", + "type": "string" + }, + { + "predicate": "University.name", + "type": "string" + }, + { + "predicate": "University.numStudents", + "type": "int" + }, + { + "predicate": "User.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Zoo.animals", + "type": "uid", + "list": true + }, + { + "predicate": "Zoo.city", + "type": "string" + }, + { + "predicate": "appears_in", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "list": true + }, + { + "predicate": "credits", + "type": "float" + }, + { + "predicate": "dgraph.cors", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ], + "list": true, + "upsert": true + }, + { + "predicate": "dgraph.drop.op", + "type": "string" + }, + { + "predicate": "dgraph.graphql.p_query", + "type": "string" + }, + { + "predicate": "dgraph.graphql.p_sha256hash", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ] + }, + { + "predicate": "dgraph.graphql.schema", + "type": "string" + }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", + "type": "string" + }, + { + "predicate": "dgraph.graphql.xid", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ], + "upsert": true + }, + { + "predicate": "dgraph.type", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ], + "list": true + }, + { + "predicate": "directed.movies", + "type": "uid", + "reverse": true, + "list": true + }, + { + "predicate": "hasStates", + "type": "uid", + "list": true + }, + { + "predicate": "inCountry", + "type": "uid" + }, + { + "predicate": "is_published", + "type": "bool", + "index": true, + "tokenizer": [ + "bool" + ] + }, + { + "predicate": "myPost.category", + "type": "uid" + }, + { + "predicate": "myPost.numLikes", + "type": "int", + "index": true, + "tokenizer": [ + "int" + ] + }, + { + "predicate": "myPost.numViews", + "type": "int", + "index": true, + "tokenizer": [ + "int" + ] + }, + { + "predicate": "myPost.postType", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ] + }, + { + "predicate": "myPost.tags", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ], + "list": true + }, + { + "predicate": "myPost.title", + "type": "string", + "index": true, + "tokenizer": [ + "fulltext", + "term" + ] + }, + { + "predicate": "performance.character.name", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ] + }, + { + "predicate": "post", + "type": "string" + }, + { + "predicate": "post.author", + "type": "uid" + }, + { + "predicate": "post1.commentsByMonth", + "type": "int", + "list": true + }, + { + "predicate": "post1.likesByMonth", + "type": "int", + "list": true + }, + { + "predicate": "post1.numLikes", + "type": "int", + "index": true, + "tokenizer": [ + "int" + ] + }, + { + "predicate": "post1.title", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ], + "upsert": true + }, + { + "predicate": "pwd", + "type": "password" + }, + { + "predicate": "roboDroid.primaryFunction", + "type": "string" + }, + { + "predicate": "star.ship.length", + "type": "float" + }, + { + "predicate": "star.ship.name", + "type": "string", + "index": true, + "tokenizer": [ + "term" + ] + }, + { + "predicate": "test.dgraph.author.country", + "type": "uid" + }, + { + "predicate": "test.dgraph.author.dob", + "type": "datetime", + "index": true, + "tokenizer": [ + "year" + ] + }, + { + "predicate": "test.dgraph.author.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ] + }, + { + "predicate": "test.dgraph.author.posts", + "type": "uid", + "list": true + }, + { + "predicate": "test.dgraph.author.qualification", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ] + }, + { + "predicate": "test.dgraph.author.reputation", + "type": "float", + "index": true, + "tokenizer": [ + "float" + ] + }, + { + "predicate": "test.dgraph.employee.en.ename", + "type": "string" + }, + { + "predicate": "test.dgraph.topic", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ] + }, + { + "predicate": "text", + "type": "string", + "index": true, + "tokenizer": [ + "fulltext" + ] + }, + { + "predicate": "职业", + "type": "string" + } + ], + "types": [ + { + "fields": [ + { + "name": "Animal.category" + } + ], + "name": "Animal" + }, + { + "fields": [ { - "predicate": "Animal.category", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ] + "name": "Category.name" }, { - "predicate": "Category.name", - "type": "string" - }, + "name": "Category.posts" + } + ], + "name": "Category" + }, + { + "fields": [ { - "predicate": "Category.posts", - "type": "uid", - "list": true + "name": "Animal.category" }, { - "predicate": "Cheetah.speed", - "type": "float" - }, + "name": "Cheetah.speed" + } + ], + "name": "Cheetah" + }, + { + "fields": [ { - "predicate": "Comment1.id", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "upsert": true + "name": "Comment1.id" }, { - "predicate": "Comment1.replies", - "type": "uid", - "list": true - }, + "name": "Comment1.replies" + } + ], + "name": "Comment1" + }, + { + "fields": [ { - "predicate": "Country.name", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ] + "name": "Country.name" }, { - "predicate": "Dog.breed", - "type": "string", - "index": true, - "tokenizer": [ - "term" - ] - }, + "name": "hasStates" + } + ], + "name": "Country" + }, + { + "fields": [ { - "predicate": "Home.address", - "type": "string" + "name": "Dataset.id" }, { - "predicate": "Home.favouriteMember", - "type": "uid" + "name": "Dataset.owner" }, { - "predicate": "Home.members", - "type": "uid", - "list": true + "name": "Dataset.project" }, { - "predicate": "Hotel.area", - "type": "geo", - "index": true, - "tokenizer": [ - "geo" - ] - }, + "name": "Dataset.name" + } + ], + "name": "Dataset" + }, + { + "fields": [ { - "predicate": "Hotel.branches", - "type": "geo", - "index": true, - "tokenizer": [ - "geo" - ] + "name": "District.id" }, { - "predicate": "Hotel.location", - "type": "geo", - "index": true, - "tokenizer": [ - "geo" - ] - }, + "name": "District.name" + } + ], + "name": "District" + }, + { + "fields": [ { - "predicate": "Hotel.name", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ] + "name": "Animal.category" }, { - "predicate": "Human.starships", - "type": "uid", - "list": true - }, + "name": "Dog.breed" + } + ], + "name": "Dog" + }, + { + "fields": [ { - "predicate": "Movie.name", - "type": "string" + "name": "Home.address" }, { - "predicate": "MovieDirector.name", - "type": "string" + "name": "Home.members" }, { - "predicate": "Parrot.repeatsWords", - "type": "string", - "list": true - }, + "name": "Home.favouriteMember" + } + ], + "name": "Home" + }, + { + "fields": [ { - "predicate": "People.name", - "type": "string" + "name": "Hotel.name" }, { - "predicate": "People.xid", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "upsert": true + "name": "Hotel.location" }, { - "predicate": "Person1.friends", - "type": "uid", - "list": true + "name": "Hotel.area" }, { - "predicate": "Person1.name", - "type": "string" - }, + "name": "Hotel.branches" + } + ], + "name": "Hotel" + }, + { + "fields": [ { - "predicate": "Plant.breed", - "type": "string" + "name": "performance.character.name" }, { - "predicate": "Post1.comments", - "type": "uid", - "list": true + "name": "appears_in" }, { - "predicate": "Post1.id", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "upsert": true + "name": "test.dgraph.employee.en.ename" }, { - "predicate": "State.capital", - "type": "string" + "name": "Human.starships" }, { - "predicate": "State.name", - "type": "string" - }, + "name": "credits" + } + ], + "name": "Human" + }, + { + "fields": [ { - "predicate": "State.xcode", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ], - "upsert": true + "name": "post" }, { - "predicate": "Student.taughtBy", - "type": "uid", - "list": true - }, + "name": "职业" + } + ], + "name": "Message" + }, + { + "fields": [ { - "predicate": "Teacher.subject", - "type": "string" - }, + "name": "Movie.name" + } + ], + "name": "Movie" + }, + { + "fields": [ { - "predicate": "Teacher.teaches", - "type": "uid", - "list": true + "name": "MovieDirector.name" }, { - "predicate": "Thing.name", - "type": "string" + "name": "directed.movies" + } + ], + "name": "MovieDirector" + }, + { + "fields": [ + { + "name": "Owner.username" }, { - "predicate": "ThingOne.color", - "type": "string" + "name": "Owner.password" }, { - "predicate": "ThingOne.usedBy", - "type": "string" + "name": "Owner.projects" + } + ], + "name": "Owner" + }, + { + "fields": [ + { + "name": "Animal.category" }, { - "predicate": "ThingTwo.color", - "type": "string" + "name": "Parrot.repeatsWords" + } + ], + "name": "Parrot" + }, + { + "fields": [ + { + "name": "People.xid" }, { - "predicate": "ThingTwo.owner", - "type": "string" + "name": "People.name" + } + ], + "name": "People" + }, + { + "fields": [ + { + "name": "Person1.name" }, { - "predicate": "University.name", - "type": "string" + "name": "Person1.friends" + } + ], + "name": "Person1" + }, + { + "fields": [ + { + "name": "Plant.breed" + } + ], + "name": "Plant" + }, + { + "fields": [ + { + "name": "Post1.id" }, { - "predicate": "University.numStudents", - "type": "int" + "name": "Post1.comments" + } + ], + "name": "Post1" + }, + { + "fields": [ + { + "name": "Project.id" }, { - "predicate": "User.name", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "upsert": true + "name": "Project.owner" }, { - "predicate": "Zoo.animals", - "type": "uid", - "list": true + "name": "Project.name" }, { - "predicate": "Zoo.city", - "type": "string" + "name": "Project.datasets" + } + ], + "name": "Project" + }, + { + "fields": [ + { + "name": "Region.id" }, { - "predicate": "appears_in", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "list": true + "name": "Region.name" }, { - "predicate": "credits", - "type": "float" + "name": "Region.district" + } + ], + "name": "Region" + }, + { + "fields": [ + { + "name": "State.xcode" }, { - "predicate": "dgraph.cors", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "list": true, - "upsert": true + "name": "State.name" }, { - "predicate": "dgraph.drop.op", - "type": "string" + "name": "State.capital" }, { - "predicate":"dgraph.graphql.p_query", - "type":"string" + "name": "State.region" }, { - "predicate":"dgraph.graphql.p_sha256hash", - "type":"string", - "index":true, - "tokenizer":["exact"] + "name": "inCountry" + } + ], + "name": "State" + }, + { + "fields": [ + { + "name": "People.xid" }, { - "predicate": "dgraph.graphql.schema", - "type": "string" + "name": "People.name" }, { - "predicate": "dgraph.graphql.schema_created_at", - "type": "datetime" + "name": "Student.taughtBy" + } + ], + "name": "Student" + }, + { + "fields": [ + { + "name": "People.xid" }, { - "predicate": "dgraph.graphql.schema_history", - "type": "string" + "name": "People.name" }, { - "predicate": "dgraph.graphql.xid", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "upsert": true + "name": "Teacher.subject" }, { - "predicate": "dgraph.type", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "list": true + "name": "Teacher.teaches" + } + ], + "name": "Teacher" + }, + { + "fields": [ + { + "name": "Thing.name" + } + ], + "name": "Thing" + }, + { + "fields": [ + { + "name": "Thing.name" }, { - "predicate": "directed.movies", - "type": "uid", - "reverse": true, - "list": true + "name": "ThingOne.color" }, { - "predicate": "hasStates", - "type": "uid", - "list": true + "name": "ThingOne.usedBy" + } + ], + "name": "ThingOne" + }, + { + "fields": [ + { + "name": "Thing.name" }, { - "predicate": "inCountry", - "type": "uid" + "name": "ThingTwo.color" }, { - "predicate": "is_published", - "type": "bool", - "index": true, - "tokenizer": [ - "bool" - ] + "name": "ThingTwo.owner" + } + ], + "name": "ThingTwo" + }, + { + "fields": [ + { + "name": "University.name" }, { - "predicate": "myPost.category", - "type": "uid" + "name": "University.numStudents" + } + ], + "name": "University" + }, + { + "fields": [ + { + "name": "User.name" }, { - "predicate": "myPost.numLikes", - "type": "int", - "index": true, - "tokenizer": [ - "int" - ] + "name": "pwd" + } + ], + "name": "User" + }, + { + "fields": [ + { + "name": "Zoo.animals" }, { - "predicate": "myPost.numViews", - "type": "int", - "index": true, - "tokenizer": [ - "int" - ] + "name": "Zoo.city" + } + ], + "name": "Zoo" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema" }, { - "predicate": "myPost.postType", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ] + "name": "dgraph.graphql.xid" + } + ], + "name": "dgraph.graphql" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" }, { - "predicate": "myPost.tags", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "list": true + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" + }, + { + "fields": [ + { + "name": "dgraph.graphql.p_query" }, { - "predicate": "myPost.title", - "type": "string", - "index": true, - "tokenizer": [ - "fulltext", - "term" - ] + "name": "dgraph.graphql.p_sha256hash" + } + ], + "name": "dgraph.graphql.persisted_query" + }, + { + "fields": [ + { + "name": "dgraph.cors" + } + ], + "name": "dgraph.type.cors" + }, + { + "fields": [ + { + "name": "myPost.title" }, { - "predicate": "performance.character.name", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ] + "name": "text" }, { - "predicate": "post", - "type": "string" + "name": "myPost.tags" }, { - "predicate": "post.author", - "type": "uid" + "name": "test.dgraph.topic" }, { - "index": true, - "predicate": "post1.numLikes", - "tokenizer": [ - "int" - ], - "type": "int" + "name": "myPost.numLikes" }, { - "predicate": "post1.title", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ], - "upsert": true + "name": "myPost.numViews" }, { - "predicate": "post1.commentsByMonth", - "list": true, - "type": "int" + "name": "is_published" }, { - "predicate": "post1.likesByMonth", - "list": true, - "type": "int" + "name": "myPost.postType" }, { - "predicate": "pwd", - "type": "password" + "name": "post.author" }, { - "predicate": "roboDroid.primaryFunction", - "type": "string" + "name": "myPost.category" + } + ], + "name": "myPost" + }, + { + "fields": [ + { + "name": "performance.character.name" }, { - "predicate": "star.ship.length", - "type": "float" + "name": "appears_in" + } + ], + "name": "performance.character" + }, + { + "fields": [ + { + "name": "post1.title" }, { - "predicate": "star.ship.name", - "type": "string", - "index": true, - "tokenizer": [ - "term" - ] + "name": "post1.numLikes" }, { - "predicate": "test.dgraph.author.country", - "type": "uid" + "name": "post1.commentsByMonth" }, { - "predicate": "test.dgraph.author.dob", - "type": "datetime", - "index": true, - "tokenizer": [ - "year" - ] + "name": "post1.likesByMonth" + } + ], + "name": "post1" + }, + { + "fields": [ + { + "name": "performance.character.name" }, { - "predicate": "test.dgraph.author.name", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ] + "name": "appears_in" }, { - "predicate": "test.dgraph.author.posts", - "type": "uid", - "list": true + "name": "roboDroid.primaryFunction" + } + ], + "name": "roboDroid" + }, + { + "fields": [ + { + "name": "star.ship.name" }, { - "predicate": "test.dgraph.author.reputation", - "type": "float", - "index": true, - "tokenizer": [ - "float" - ] + "name": "star.ship.length" + } + ], + "name": "star.ship" + }, + { + "fields": [ + { + "name": "test.dgraph.author.name" }, { - "index": true, - "predicate": "test.dgraph.author.qualification", - "tokenizer": [ - "hash", - "trigram" - ], - "type": "string" + "name": "test.dgraph.author.dob" }, { - "predicate": "test.dgraph.employee.en.ename", - "type": "string" + "name": "test.dgraph.author.reputation" }, { - "predicate": "test.dgraph.topic", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ] + "name": "test.dgraph.author.qualification" }, { - "predicate": "text", - "type": "string", - "index": true, - "tokenizer": [ - "fulltext" - ] - }, + "name": "test.dgraph.author.country" + }, { - "predicate": "职业", - "type": "string" + "name": "test.dgraph.author.posts" } - ], - "types": [ + ], + "name": "test.dgraph.author" + }, + { + "fields": [ { - "fields": [ - { - "name": "Animal.category" - } - ], - "name": "Animal" - }, - { - "fields": [ - { - "name": "Category.name" - }, - { - "name": "Category.posts" - } - ], - "name": "Category" - }, - { - "fields": [ - { - "name": "Animal.category" - }, - { - "name": "Cheetah.speed" - } - ], - "name": "Cheetah" - }, - { - "fields": [ - { - "name": "Comment1.id" - }, - { - "name": "Comment1.replies" - } - ], - "name": "Comment1" - }, - { - "fields": [ - { - "name": "Country.name" - }, - { - "name": "hasStates" - } - ], - "name": "Country" - }, - { - "fields": [ - { - "name": "Animal.category" - }, - { - "name": "Dog.breed" - } - ], - "name": "Dog" - }, - { - "fields": [ - { - "name": "Home.address" - }, - { - "name": "Home.members" - }, - { - "name": "Home.favouriteMember" - } - ], - "name": "Home" - }, - { - "fields": [ - { - "name": "Hotel.name" - }, - { - "name": "Hotel.location" - }, - { - "name": "Hotel.area" - }, - { - "name": "Hotel.branches" - } - ], - "name": "Hotel" - }, - { - "fields": [ - { - "name": "test.dgraph.employee.en.ename" - }, - { - "name": "performance.character.name" - }, - { - "name": "appears_in" - }, - { - "name": "Human.starships" - }, - { - "name": "credits" - } - ], - "name": "Human" - }, - { - "fields": [ - { - "name": "post" - }, - { - "name": "职业" - } - ], - "name": "Message" - }, - { - "fields": [ - { - "name": "Movie.name" - } - ], - "name": "Movie" - }, - { - "fields": [ - { - "name": "MovieDirector.name" - }, - { - "name": "directed.movies" - } - ], - "name": "MovieDirector" - }, - { - "fields": [ - { - "name": "Animal.category" - }, - { - "name": "Parrot.repeatsWords" - } - ], - "name": "Parrot" - }, - { - "fields": [ - { - "name": "People.xid" - }, - { - "name": "People.name" - } - ], - "name": "People" - }, - { - "fields": [ - { - "name": "Person1.name" - }, - { - "name": "Person1.friends" - } - ], - "name": "Person1" - }, - { - "fields": [ - { - "name": "Plant.breed" - } - ], - "name": "Plant" - }, - { - "fields": [ - { - "name": "Post1.id" - }, - { - "name": "Post1.comments" - } - ], - "name": "Post1" - }, - { - "fields": [ - { - "name": "State.xcode" - }, - { - "name": "State.name" - }, - { - "name": "State.capital" - }, - { - "name": "inCountry" - } - ], - "name": "State" - }, - { - "fields": [ - { - "name": "People.xid" - }, - { - "name": "People.name" - }, - { - "name": "Student.taughtBy" - } - ], - "name": "Student" - }, - { - "fields": [ - { - "name": "People.xid" - }, - { - "name": "People.name" - }, - { - "name": "Teacher.subject" - }, - { - "name": "Teacher.teaches" - } - ], - "name": "Teacher" - }, - { - "fields": [ - { - "name": "Thing.name" - } - ], - "name": "Thing" - }, - { - "fields": [ - { - "name": "Thing.name" - }, - { - "name": "ThingOne.color" - }, - { - "name": "ThingOne.usedBy" - } - ], - "name": "ThingOne" - }, - { - "fields": [ - { - "name": "Thing.name" - }, - { - "name": "ThingTwo.color" - }, - { - "name": "ThingTwo.owner" - } - ], - "name": "ThingTwo" - }, - { - "fields": [ - { - "name": "University.name" - }, - { - "name": "University.numStudents" - } - ], - "name": "University" - }, - { - "fields": [ - { - "name": "User.name" - }, - { - "name": "pwd" - } - ], - "name": "User" - }, - { - "fields": [ - { - "name": "Zoo.animals" - }, - { - "name": "Zoo.city" - } - ], - "name": "Zoo" - }, - { - "fields": [ - { - "name": "dgraph.graphql.schema" - }, - { - "name": "dgraph.graphql.xid" - } - ], - "name": "dgraph.graphql" - }, - { - "fields": [ - { - "name": "dgraph.graphql.schema_history" - }, - { - "name": "dgraph.graphql.schema_created_at" - } - ], - "name": "dgraph.graphql.history" - }, - { - "fields": [ - { - "name": "dgraph.graphql.p_query" - }, - { - "name": "dgraph.graphql.p_sha256hash" - } - ], - "name": "dgraph.graphql.persisted_query" - }, - { - "fields": [ - { - "name": "myPost.title" - }, - { - "name": "text" - }, - { - "name": "myPost.tags" - }, - { - "name": "test.dgraph.topic" - }, - { - "name": "myPost.numLikes" - }, - { - "name": "myPost.numViews" - }, - { - "name": "is_published" - }, - { - "name": "myPost.postType" - }, - { - "name": "post.author" - }, - { - "name": "myPost.category" - } - ], - "name": "myPost" - }, - { - "fields": [ - { - "name": "performance.character.name" - }, - { - "name": "appears_in" - } - ], - "name": "performance.character" - }, - { - "fields": [ - { - "name": "post1.title" - }, - { - "name": "post1.numLikes" - }, - { - "name": "post1.commentsByMonth" - }, - { - "name": "post1.likesByMonth" - } - - ], - "name": "post1" - }, - { - "fields": [ - { - "name": "performance.character.name" - }, - { - "name": "appears_in" - }, - { - "name": "roboDroid.primaryFunction" - } - ], - "name": "roboDroid" - }, - { - "fields": [ - { - "name": "star.ship.name" - }, - { - "name": "star.ship.length" - } - ], - "name": "star.ship" - }, - { - "fields": [ - { - "name": "test.dgraph.author.name" - }, - { - "name": "test.dgraph.author.dob" - }, - { - "name": "test.dgraph.author.reputation" - }, - { - "name": "test.dgraph.author.country" - }, - { - "name": "test.dgraph.author.posts" - }, - { - "name": "test.dgraph.author.qualification" - } - ], - "name": "test.dgraph.author" - }, - { - "fields": [ - { - "name": "test.dgraph.employee.en.ename" - } - ], - "name": "test.dgraph.employee.en" - }, - { - "fields": [ - { - "name": "dgraph.cors" - } - ], - "name": "dgraph.type.cors" - } - ] -} \ No newline at end of file + "name": "test.dgraph.employee.en.ename" + } + ], + "name": "test.dgraph.employee.en" + } + ] +} diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index ff972c0f335..d699ff0ea18 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -21,6 +21,7 @@ type State { xcode: String! @id @search(by: [regexp]) name: String! capital: String + region: Region country: Country } @@ -284,3 +285,34 @@ type University @generate( name: String! numStudents: Int } + +type Region { + id: String! @id + name: String! + district: District +} + +type District { + id: String! @id + name: String! +} + +type Owner { + username: String! @id + password: String! + projects: [Project!] @hasInverse(field: owner) +} + +type Project { + id: String! @id + owner: Owner! + name: String! @search(by: [hash]) + datasets: [Dataset!] @hasInverse(field: project) +} + +type Dataset { + id: String! @id + owner: Owner! + project: Project! + name: String! @search(by: [hash]) +} diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index 156a9c139b9..d6cdbc0b436 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -1,980 +1,1161 @@ { - "schema": [ + "schema": [ + { + "predicate": "Animal.category", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ] + }, + { + "predicate": "Author.country", + "type": "uid" + }, + { + "predicate": "Author.dob", + "type": "datetime", + "index": true, + "tokenizer": [ + "year" + ] + }, + { + "predicate": "Author.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ] + }, + { + "predicate": "Author.posts", + "type": "uid", + "list": true + }, + { + "predicate": "Author.qualification", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ] + }, + { + "predicate": "Author.reputation", + "type": "float", + "index": true, + "tokenizer": [ + "float" + ] + }, + { + "predicate": "Category.name", + "type": "string" + }, + { + "predicate": "Category.posts", + "type": "uid", + "list": true + }, + { + "predicate": "Character.appearsIn", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "list": true + }, + { + "predicate": "Character.name", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ] + }, + { + "predicate": "Cheetah.speed", + "type": "float" + }, + { + "predicate": "Comment1.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Comment1.replies", + "type": "uid", + "list": true + }, + { + "predicate": "Country.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ] + }, + { + "predicate": "Country.states", + "type": "uid", + "list": true + }, + { + "predicate": "Dataset.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Dataset.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ] + }, + { + "predicate": "Dataset.owner", + "type": "uid" + }, + { + "predicate": "Dataset.project", + "type": "uid" + }, + { + "predicate": "District.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "District.name", + "type": "string" + }, + { + "predicate": "Dog.breed", + "type": "string", + "index": true, + "tokenizer": [ + "term" + ] + }, + { + "predicate": "Droid.primaryFunction", + "type": "string" + }, + { + "predicate": "Employee.ename", + "type": "string" + }, + { + "predicate": "Home.address", + "type": "string" + }, + { + "predicate": "Home.favouriteMember", + "type": "uid" + }, + { + "predicate": "Home.members", + "type": "uid", + "list": true + }, + { + "predicate": "Hotel.area", + "type": "geo", + "index": true, + "tokenizer": [ + "geo" + ] + }, + { + "predicate": "Hotel.branches", + "type": "geo", + "index": true, + "tokenizer": [ + "geo" + ] + }, + { + "predicate": "Hotel.location", + "type": "geo", + "index": true, + "tokenizer": [ + "geo" + ] + }, + { + "predicate": "Hotel.name", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ] + }, + { + "predicate": "Human.starships", + "type": "uid", + "list": true + }, + { + "predicate": "Human.totalCredits", + "type": "float" + }, + { + "predicate": "Movie.director", + "type": "uid", + "list": true + }, + { + "predicate": "Movie.name", + "type": "string" + }, + { + "predicate": "MovieDirector.directed", + "type": "uid", + "list": true + }, + { + "predicate": "MovieDirector.name", + "type": "string" + }, + { + "predicate": "Owner.password", + "type": "string" + }, + { + "predicate": "Owner.projects", + "type": "uid", + "list": true + }, + { + "predicate": "Owner.username", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Parrot.repeatsWords", + "type": "string", + "list": true + }, + { + "predicate": "People.name", + "type": "string" + }, + { + "predicate": "People.xid", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Person.name", + "type": "string" + }, + { + "predicate": "Person1.friends", + "type": "uid", + "list": true + }, + { + "predicate": "Person1.name", + "type": "string" + }, + { + "predicate": "Plant.breed", + "type": "string" + }, + { + "predicate": "Post.author", + "type": "uid" + }, + { + "predicate": "Post.category", + "type": "uid" + }, + { + "predicate": "Post.isPublished", + "type": "bool", + "index": true, + "tokenizer": [ + "bool" + ] + }, + { + "predicate": "Post.numLikes", + "type": "int", + "index": true, + "tokenizer": [ + "int" + ] + }, + { + "predicate": "Post.numViews", + "type": "int", + "index": true, + "tokenizer": [ + "int" + ] + }, + { + "predicate": "Post.postType", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ] + }, + { + "predicate": "Post.tags", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ], + "list": true + }, + { + "predicate": "Post.text", + "type": "string", + "index": true, + "tokenizer": [ + "fulltext" + ] + }, + { + "predicate": "Post.title", + "type": "string", + "index": true, + "tokenizer": [ + "fulltext", + "term" + ] + }, + { + "predicate": "Post.topic", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ] + }, + { + "predicate": "Post1.comments", + "type": "uid", + "list": true + }, + { + "predicate": "Post1.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Project.datasets", + "type": "uid", + "list": true + }, + { + "predicate": "Project.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Project.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ] + }, + { + "predicate": "Project.owner", + "type": "uid" + }, + { + "predicate": "Region.district", + "type": "uid" + }, + { + "predicate": "Region.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Region.name", + "type": "string" + }, + { + "predicate": "Starship.length", + "type": "float" + }, + { + "predicate": "Starship.name", + "type": "string", + "index": true, + "tokenizer": [ + "term" + ] + }, + { + "predicate": "State.capital", + "type": "string" + }, + { + "predicate": "State.country", + "type": "uid" + }, + { + "predicate": "State.name", + "type": "string" + }, + { + "predicate": "State.region", + "type": "uid" + }, + { + "predicate": "State.xcode", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ], + "upsert": true + }, + { + "predicate": "Student.taughtBy", + "type": "uid", + "list": true + }, + { + "predicate": "Teacher.subject", + "type": "string" + }, + { + "predicate": "Teacher.teaches", + "type": "uid", + "list": true + }, + { + "predicate": "Thing.name", + "type": "string" + }, + { + "predicate": "ThingOne.color", + "type": "string" + }, + { + "predicate": "ThingOne.usedBy", + "type": "string" + }, + { + "predicate": "ThingTwo.color", + "type": "string" + }, + { + "predicate": "ThingTwo.owner", + "type": "string" + }, + { + "predicate": "University.name", + "type": "string" + }, + { + "predicate": "University.numStudents", + "type": "int" + }, + { + "predicate": "User.name", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "User.password", + "type": "password" + }, + { + "predicate": "Zoo.animals", + "type": "uid", + "list": true + }, + { + "predicate": "Zoo.city", + "type": "string" + }, + { + "predicate": "dgraph.cors", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ], + "list": true, + "upsert": true + }, + { + "predicate": "dgraph.drop.op", + "type": "string" + }, + { + "predicate": "dgraph.graphql.p_query", + "type": "string" + }, + { + "predicate": "dgraph.graphql.p_sha256hash", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ] + }, + { + "predicate": "dgraph.graphql.schema", + "type": "string" + }, + { + "predicate": "dgraph.graphql.schema_created_at", + "type": "datetime" + }, + { + "predicate": "dgraph.graphql.schema_history", + "type": "string" + }, + { + "predicate": "dgraph.graphql.xid", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ], + "upsert": true + }, + { + "predicate": "dgraph.type", + "type": "string", + "index": true, + "tokenizer": [ + "exact" + ], + "list": true + }, + { + "predicate": "post1.commentsByMonth", + "type": "int", + "list": true + }, + { + "predicate": "post1.likesByMonth", + "type": "int", + "list": true + }, + { + "predicate": "post1.numLikes", + "type": "int", + "index": true, + "tokenizer": [ + "int" + ] + }, + { + "predicate": "post1.title", + "type": "string", + "index": true, + "tokenizer": [ + "hash", + "trigram" + ], + "upsert": true + } + ], + "types": [ + { + "fields": [ + { + "name": "Animal.category" + } + ], + "name": "Animal" + }, + { + "fields": [ { - "predicate": "Animal.category", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ] + "name": "Author.name" }, { - "predicate": "Author.country", - "type": "uid" + "name": "Author.dob" }, { - "predicate": "Author.dob", - "type": "datetime", - "index": true, - "tokenizer": [ - "year" - ] + "name": "Author.reputation" }, { - "predicate": "Author.name", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ] + "name": "Author.qualification" }, { - "predicate": "Author.posts", - "type": "uid", - "list": true + "name": "Author.country" }, { - "predicate": "Author.reputation", - "type": "float", - "index": true, - "tokenizer": [ - "float" - ] - }, + "name": "Author.posts" + } + ], + "name": "Author" + }, + { + "fields": [ { - "index": true, - "predicate": "Author.qualification", - "tokenizer": [ - "hash", - "trigram" - ], - "type": "string" + "name": "Category.name" }, { - "predicate": "Category.name", - "type": "string" - }, + "name": "Category.posts" + } + ], + "name": "Category" + }, + { + "fields": [ { - "predicate": "Category.posts", - "type": "uid", - "list": true + "name": "Character.name" }, { - "predicate": "Character.appearsIn", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "list": true - }, + "name": "Character.appearsIn" + } + ], + "name": "Character" + }, + { + "fields": [ { - "predicate": "Character.name", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ] + "name": "Animal.category" }, { - "predicate": "Cheetah.speed", - "type": "float" - }, + "name": "Cheetah.speed" + } + ], + "name": "Cheetah" + }, + { + "fields": [ { - "predicate": "Comment1.id", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "upsert": true + "name": "Comment1.id" }, { - "predicate": "Comment1.replies", - "type": "uid", - "list": true - }, + "name": "Comment1.replies" + } + ], + "name": "Comment1" + }, + { + "fields": [ { - "predicate": "Country.name", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ] + "name": "Country.name" }, { - "predicate": "Country.states", - "type": "uid", - "list": true - }, + "name": "Country.states" + } + ], + "name": "Country" + }, + { + "fields": [ { - "predicate": "Dog.breed", - "type": "string", - "index": true, - "tokenizer": [ - "term" - ] + "name": "Dataset.id" }, { - "predicate": "Droid.primaryFunction", - "type": "string" + "name": "Dataset.owner" }, { - "predicate": "Employee.ename", - "type": "string" + "name": "Dataset.project" }, { - "predicate": "Home.address", - "type": "string" - }, + "name": "Dataset.name" + } + ], + "name": "Dataset" + }, + { + "fields": [ { - "predicate": "Home.favouriteMember", - "type": "uid" + "name": "District.id" }, { - "predicate": "Home.members", - "type": "uid", - "list": true - }, + "name": "District.name" + } + ], + "name": "District" + }, + { + "fields": [ { - "predicate": "Hotel.area", - "type": "geo", - "index": true, - "tokenizer": [ - "geo" - ] + "name": "Animal.category" }, { - "predicate": "Hotel.branches", - "type": "geo", - "index": true, - "tokenizer": [ - "geo" - ] - }, + "name": "Dog.breed" + } + ], + "name": "Dog" + }, + { + "fields": [ { - "predicate": "Hotel.location", - "type": "geo", - "index": true, - "tokenizer": [ - "geo" - ] + "name": "Character.name" }, { - "predicate": "Hotel.name", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ] + "name": "Character.appearsIn" }, { - "predicate": "Human.starships", - "type": "uid", - "list": true - }, + "name": "Droid.primaryFunction" + } + ], + "name": "Droid" + }, + { + "fields": [ { - "predicate": "Human.totalCredits", - "type": "float" - }, + "name": "Employee.ename" + } + ], + "name": "Employee" + }, + { + "fields": [ { - "predicate": "Movie.director", - "type": "uid", - "list": true + "name": "Home.address" }, { - "predicate": "Movie.name", - "type": "string" + "name": "Home.members" }, { - "predicate": "MovieDirector.directed", - "type": "uid", - "list": true - }, + "name": "Home.favouriteMember" + } + ], + "name": "Home" + }, + { + "fields": [ { - "predicate": "MovieDirector.name", - "type": "string" + "name": "Hotel.name" }, { - "predicate": "Parrot.repeatsWords", - "type": "string", - "list": true + "name": "Hotel.location" }, { - "predicate": "People.name", - "type": "string" + "name": "Hotel.area" }, { - "predicate": "People.xid", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "upsert": true + "name": "Hotel.branches" + } + ], + "name": "Hotel" + }, + { + "fields": [ + { + "name": "Character.name" }, { - "predicate": "Person.name", - "type": "string" + "name": "Character.appearsIn" }, { - "predicate": "Person1.friends", - "type": "uid", - "list": true + "name": "Employee.ename" }, { - "predicate": "Person1.name", - "type": "string" + "name": "Human.starships" }, { - "predicate": "Plant.breed", - "type": "string" + "name": "Human.totalCredits" + } + ], + "name": "Human" + }, + { + "fields": [ + { + "name": "Movie.name" }, { - "predicate": "Post.author", - "type": "uid" + "name": "Movie.director" + } + ], + "name": "Movie" + }, + { + "fields": [ + { + "name": "MovieDirector.name" }, { - "predicate": "Post.category", - "type": "uid" + "name": "MovieDirector.directed" + } + ], + "name": "MovieDirector" + }, + { + "fields": [ + { + "name": "Owner.username" }, { - "predicate": "Post.isPublished", - "type": "bool", - "index": true, - "tokenizer": [ - "bool" - ] + "name": "Owner.password" }, { - "predicate": "Post.numLikes", - "type": "int", - "index": true, - "tokenizer": [ - "int" - ] + "name": "Owner.projects" + } + ], + "name": "Owner" + }, + { + "fields": [ + { + "name": "Animal.category" }, { - "predicate": "Post.numViews", - "type": "int", - "index": true, - "tokenizer": [ - "int" - ] + "name": "Parrot.repeatsWords" + } + ], + "name": "Parrot" + }, + { + "fields": [ + { + "name": "People.xid" }, { - "predicate": "Post.postType", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ] + "name": "People.name" + } + ], + "name": "People" + }, + { + "fields": [ + { + "name": "Person.name" + } + ], + "name": "Person" + }, + { + "fields": [ + { + "name": "Person1.name" }, { - "predicate": "Post.tags", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "list": true + "name": "Person1.friends" + } + ], + "name": "Person1" + }, + { + "fields": [ + { + "name": "Plant.breed" + } + ], + "name": "Plant" + }, + { + "fields": [ + { + "name": "Post.title" }, { - "predicate": "Post.text", - "type": "string", - "index": true, - "tokenizer": [ - "fulltext" - ] + "name": "Post.text" }, { - "predicate": "Post.title", - "type": "string", - "index": true, - "tokenizer": [ - "fulltext", - "term" - ] + "name": "Post.tags" }, { - "predicate": "Post.topic", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ] + "name": "Post.topic" }, { - "predicate": "Post1.comments", - "type": "uid", - "list": true + "name": "Post.numLikes" }, { - "predicate": "Post1.id", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "upsert": true + "name": "Post.numViews" }, { - "predicate": "Starship.length", - "type": "float" + "name": "Post.isPublished" }, { - "predicate": "Starship.name", - "type": "string", - "index": true, - "tokenizer": [ - "term" - ] + "name": "Post.postType" }, { - "predicate": "State.capital", - "type": "string" + "name": "Post.author" }, { - "predicate": "State.country", - "type": "uid" + "name": "Post.category" + } + ], + "name": "Post" + }, + { + "fields": [ + { + "name": "Post1.id" }, { - "predicate": "State.name", - "type": "string" + "name": "Post1.comments" + } + ], + "name": "Post1" + }, + { + "fields": [ + { + "name": "Project.id" }, { - "predicate": "State.xcode", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ], - "upsert": true + "name": "Project.owner" }, { - "predicate": "Student.taughtBy", - "type": "uid", - "list": true + "name": "Project.name" }, { - "predicate": "Teacher.subject", - "type": "string" + "name": "Project.datasets" + } + ], + "name": "Project" + }, + { + "fields": [ + { + "name": "Region.id" }, { - "predicate": "Teacher.teaches", - "type": "uid", - "list": true + "name": "Region.name" }, { - "predicate": "Thing.name", - "type": "string" + "name": "Region.district" + } + ], + "name": "Region" + }, + { + "fields": [ + { + "name": "Starship.name" }, { - "predicate": "ThingOne.color", - "type": "string" + "name": "Starship.length" + } + ], + "name": "Starship" + }, + { + "fields": [ + { + "name": "State.xcode" }, { - "predicate": "ThingOne.usedBy", - "type": "string" + "name": "State.name" }, { - "predicate": "ThingTwo.color", - "type": "string" + "name": "State.capital" }, { - "predicate": "ThingTwo.owner", - "type": "string" + "name": "State.region" }, { - "predicate": "University.name", - "type": "string" + "name": "State.country" + } + ], + "name": "State" + }, + { + "fields": [ + { + "name": "People.xid" }, { - "predicate": "University.numStudents", - "type": "int" + "name": "People.name" }, { - "predicate": "User.name", - "type": "string", - "index": true, - "tokenizer": [ - "hash" - ], - "upsert": true + "name": "Student.taughtBy" + } + ], + "name": "Student" + }, + { + "fields": [ + { + "name": "People.xid" }, { - "predicate": "User.password", - "type": "password" + "name": "People.name" }, { - "predicate": "Zoo.animals", - "type": "uid", - "list": true + "name": "Teacher.subject" }, { - "predicate": "Zoo.city", - "type": "string" + "name": "Teacher.teaches" + } + ], + "name": "Teacher" + }, + { + "fields": [ + { + "name": "Thing.name" + } + ], + "name": "Thing" + }, + { + "fields": [ + { + "name": "Thing.name" }, { - "predicate": "dgraph.cors", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "list": true, - "upsert": true + "name": "ThingOne.color" }, { - "predicate": "dgraph.drop.op", - "type": "string" + "name": "ThingOne.usedBy" + } + ], + "name": "ThingOne" + }, + { + "fields": [ + { + "name": "Thing.name" }, { - "predicate": "dgraph.graphql.schema", - "type": "string" + "name": "ThingTwo.color" }, { - "predicate": "dgraph.graphql.schema_created_at", - "type": "datetime" + "name": "ThingTwo.owner" + } + ], + "name": "ThingTwo" + }, + { + "fields": [ + { + "name": "University.name" }, { - "predicate": "dgraph.graphql.schema_history", - "type": "string" + "name": "University.numStudents" + } + ], + "name": "University" + }, + { + "fields": [ + { + "name": "User.name" }, { - "predicate":"dgraph.graphql.p_query", - "type":"string" + "name": "User.password" + } + ], + "name": "User" + }, + { + "fields": [ + { + "name": "Zoo.animals" }, { - "predicate":"dgraph.graphql.p_sha256hash", - "type":"string", - "index":true, - "tokenizer":["exact"] + "name": "Zoo.city" + } + ], + "name": "Zoo" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema" }, { - "predicate": "dgraph.graphql.xid", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "upsert": true + "name": "dgraph.graphql.xid" + } + ], + "name": "dgraph.graphql" + }, + { + "fields": [ + { + "name": "dgraph.graphql.schema_history" }, { - "predicate": "dgraph.type", - "type": "string", - "index": true, - "tokenizer": [ - "exact" - ], - "list": true + "name": "dgraph.graphql.schema_created_at" + } + ], + "name": "dgraph.graphql.history" + }, + { + "fields": [ + { + "name": "dgraph.graphql.p_query" }, { - "index": true, - "predicate": "post1.numLikes", - "tokenizer": [ - "int" - ], - "type": "int" + "name": "dgraph.graphql.p_sha256hash" + } + ], + "name": "dgraph.graphql.persisted_query" + }, + { + "fields": [ + { + "name": "dgraph.cors" + } + ], + "name": "dgraph.type.cors" + }, + { + "fields": [ + { + "name": "post1.title" }, { - "predicate": "post1.title", - "type": "string", - "index": true, - "tokenizer": [ - "hash", - "trigram" - ], - "upsert": true + "name": "post1.numLikes" }, { - "predicate": "post1.commentsByMonth", - "list": true, - "type": "int" + "name": "post1.commentsByMonth" }, { - "predicate": "post1.likesByMonth", - "list": true, - "type": "int" + "name": "post1.likesByMonth" } - ], - "types": [ - { - "fields": [ - { - "name": "Animal.category" - } - ], - "name": "Animal" - }, - { - "fields": [ - { - "name": "Author.name" - }, - { - "name": "Author.dob" - }, - { - "name": "Author.reputation" - }, - { - "name": "Author.country" - }, - { - "name": "Author.posts" - }, - { - "name": "Author.qualification" - } - ], - "name": "Author" - }, - { - "fields": [ - { - "name": "Category.name" - }, - { - "name": "Category.posts" - } - ], - "name": "Category" - }, - { - "fields": [ - { - "name": "Character.name" - }, - { - "name": "Character.appearsIn" - } - ], - "name": "Character" - }, - { - "fields": [ - { - "name": "Animal.category" - }, - { - "name": "Cheetah.speed" - } - ], - "name": "Cheetah" - }, - { - "fields": [ - { - "name": "Comment1.id" - }, - { - "name": "Comment1.replies" - } - ], - "name": "Comment1" - }, - { - "fields": [ - { - "name": "Country.name" - }, - { - "name": "Country.states" - } - ], - "name": "Country" - }, - { - "fields": [ - { - "name": "Animal.category" - }, - { - "name": "Dog.breed" - } - ], - "name": "Dog" - }, - { - "fields": [ - { - "name": "Character.name" - }, - { - "name": "Character.appearsIn" - }, - { - "name": "Droid.primaryFunction" - } - ], - "name": "Droid" - }, - { - "fields": [ - { - "name": "Employee.ename" - } - ], - "name": "Employee" - }, - { - "fields": [ - { - "name": "Home.address" - }, - { - "name": "Home.members" - }, - { - "name": "Home.favouriteMember" - } - ], - "name": "Home" - }, - { - "fields": [ - { - "name": "Hotel.name" - }, - { - "name": "Hotel.location" - }, - { - "name": "Hotel.area" - }, - { - "name": "Hotel.branches" - } - ], - "name": "Hotel" - }, - { - "fields": [ - { - "name": "Employee.ename" - }, - { - "name": "Character.name" - }, - { - "name": "Character.appearsIn" - }, - { - "name": "Human.starships" - }, - { - "name": "Human.totalCredits" - } - ], - "name": "Human" - }, - { - "fields": [ - { - "name": "Movie.name" - }, - { - "name": "Movie.director" - } - ], - "name": "Movie" - }, - { - "fields": [ - { - "name": "MovieDirector.name" - }, - { - "name": "MovieDirector.directed" - } - ], - "name": "MovieDirector" - }, - { - "fields": [ - { - "name": "Animal.category" - }, - { - "name": "Parrot.repeatsWords" - } - ], - "name": "Parrot" - }, - { - "fields": [ - { - "name": "People.xid" - }, - { - "name": "People.name" - } - ], - "name": "People" - }, - { - "fields": [ - { - "name": "Person.name" - } - ], - "name": "Person" - }, - { - "fields": [ - { - "name": "Person1.name" - }, - { - "name": "Person1.friends" - } - ], - "name": "Person1" - }, - { - "fields": [ - { - "name": "Plant.breed" - } - ], - "name": "Plant" - }, - { - "fields": [ - { - "name": "Post.title" - }, - { - "name": "Post.text" - }, - { - "name": "Post.tags" - }, - { - "name": "Post.topic" - }, - { - "name": "Post.numLikes" - }, - { - "name": "Post.numViews" - }, - { - "name": "Post.isPublished" - }, - { - "name": "Post.postType" - }, - { - "name": "Post.author" - }, - { - "name": "Post.category" - } - ], - "name": "Post" - }, - { - "fields": [ - { - "name": "Post1.id" - }, - { - "name": "Post1.comments" - } - ], - "name": "Post1" - }, - { - "fields": [ - { - "name": "Starship.name" - }, - { - "name": "Starship.length" - } - ], - "name": "Starship" - }, - { - "fields": [ - { - "name": "State.xcode" - }, - { - "name": "State.name" - }, - { - "name": "State.capital" - }, - { - "name": "State.country" - } - ], - "name": "State" - }, - { - "fields": [ - { - "name": "People.xid" - }, - { - "name": "People.name" - }, - { - "name": "Student.taughtBy" - } - ], - "name": "Student" - }, - { - "fields": [ - { - "name": "People.xid" - }, - { - "name": "People.name" - }, - { - "name": "Teacher.subject" - }, - { - "name": "Teacher.teaches" - } - ], - "name": "Teacher" - }, - { - "fields": [ - { - "name": "Thing.name" - } - ], - "name": "Thing" - }, - { - "fields": [ - { - "name": "Thing.name" - }, - { - "name": "ThingOne.color" - }, - { - "name": "ThingOne.usedBy" - } - ], - "name": "ThingOne" - }, - { - "fields": [ - { - "name": "Thing.name" - }, - { - "name": "ThingTwo.color" - }, - { - "name": "ThingTwo.owner" - } - ], - "name": "ThingTwo" - }, - { - "fields": [ - { - "name": "University.name" - }, - { - "name": "University.numStudents" - } - ], - "name": "University" - }, - { - "fields": [ - { - "name": "User.name" - }, - { - "name": "User.password" - } - ], - "name": "User" - }, - { - "fields": [ - { - "name": "Zoo.animals" - }, - { - "name": "Zoo.city" - } - ], - "name": "Zoo" - }, - { - "fields": [ - { - "name": "dgraph.graphql.schema" - }, - { - "name": "dgraph.graphql.xid" - } - ], - "name": "dgraph.graphql" - }, - { - "fields": [ - { - "name": "dgraph.graphql.schema_history" - }, - { - "name": "dgraph.graphql.schema_created_at" - } - ], - "name": "dgraph.graphql.history" - }, - { - "fields": [ - { - "name": "dgraph.graphql.p_query" - }, - { - "name": "dgraph.graphql.p_sha256hash" - } - ], - "name": "dgraph.graphql.persisted_query" - }, - { - "fields": [ - { - "name": "post1.title" - }, - { - "name": "post1.numLikes" - }, - { - "name": "post1.commentsByMonth" - }, - { - "name": "post1.likesByMonth" - } - ], - "name": "post1" - }, - { - "fields": [ - { - "name": "dgraph.cors" - } - ], - "name": "dgraph.type.cors" - } - ] -} \ No newline at end of file + ], + "name": "post1" + } + ] +} diff --git a/graphql/resolve/add_mutation_test.yaml b/graphql/resolve/add_mutation_test.yaml index e8b8d10ece7..778384a2915 100644 --- a/graphql/resolve/add_mutation_test.yaml +++ b/graphql/resolve/add_mutation_test.yaml @@ -18,6 +18,7 @@ "location": { "latitude": 11.11 , "longitude" : 22.22} } } + qnametouid: | explanation: "Add mutation should convert the Point type mutation to corresponding Dgraph JSON mutation" dgmutations: - setjson: | @@ -249,223 +250,163 @@ } } explanation: "A uid and type should get injected and all data transformed to - underlying Dgraph edge names" + underlying Dgraph edge names. Some PostSecrets are present and are not created." dgquery: |- query { - PostSecret4 as PostSecret4(func: eq(PostSecret.title, "ps1")) @filter(type(PostSecret)) { + PostSecret1(func: eq(PostSecret.title, "ps1")) @filter(type(PostSecret)) { uid } - PostSecret7 as PostSecret7(func: eq(PostSecret.title, "ps2")) @filter(type(PostSecret)) { + PostSecret2(func: eq(PostSecret.title, "ps2")) @filter(type(PostSecret)) { uid } - PostSecret10 as PostSecret10(func: eq(PostSecret.title, "ps3")) @filter(type(PostSecret)) { + PostSecret3(func: eq(PostSecret.title, "ps3")) @filter(type(PostSecret)) { uid } - PostSecret13 as PostSecret13(func: eq(PostSecret.title, "ps4")) @filter(type(PostSecret)) { + PostSecret4(func: eq(PostSecret.title, "ps4")) @filter(type(PostSecret)) { uid } - PostSecret16 as PostSecret16(func: eq(PostSecret.title, "ps5")) @filter(type(PostSecret)) { + PostSecret5(func: eq(PostSecret.title, "ps5")) @filter(type(PostSecret)) { uid } - PostSecret19 as PostSecret19(func: eq(PostSecret.title, "ps6")) @filter(type(PostSecret)) { + PostSecret6(func: eq(PostSecret.title, "ps6")) @filter(type(PostSecret)) { uid } - PostSecret22 as PostSecret22(func: eq(PostSecret.title, "ps7")) @filter(type(PostSecret)) { + PostSecret7(func: eq(PostSecret.title, "ps7")) @filter(type(PostSecret)) { uid } - PostSecret25 as PostSecret25(func: eq(PostSecret.title, "ps8")) @filter(type(PostSecret)) { + PostSecret8(func: eq(PostSecret.title, "ps8")) @filter(type(PostSecret)) { uid } } - dgquerysec: |- - query { - PostSecret4 as PostSecret4(func: eq(PostSecret.title, "ps1")) @filter(type(PostSecret)) { - uid - } - PostSecret7 as PostSecret7(func: eq(PostSecret.title, "ps2")) @filter(type(PostSecret)) { - uid - } - PostSecret10 as PostSecret10(func: eq(PostSecret.title, "ps3")) @filter(type(PostSecret)) { - uid - } - PostSecret13 as PostSecret13(func: eq(PostSecret.title, "ps4")) @filter(type(PostSecret)) { - uid - } - PostSecret16 as PostSecret16(func: eq(PostSecret.title, "ps5")) @filter(type(PostSecret)) { - uid - } - PostSecret19 as PostSecret19(func: eq(PostSecret.title, "ps6")) @filter(type(PostSecret)) { - uid - } - PostSecret22 as PostSecret22(func: eq(PostSecret.title, "ps7")) @filter(type(PostSecret)) { - uid - } - PostSecret25 as PostSecret25(func: eq(PostSecret.title, "ps8")) @filter(type(PostSecret)) { - uid - } + qnametouid: | + { + "PostSecret1":"0x1", + "PostSecret2":"0x2", + "PostSecret3":"0x3", + "PostSecret4":"0x4" } dgmutations: - setjson: | { - "PostSecret.title": "ps1", - "dgraph.type": [ - "PostSecret" - ], - "uid": "_:PostSecret4" - } - cond: "@if(eq(len(PostSecret4), 0))" - - setjson: | - { - "PostSecret.title": "ps2", - "dgraph.type": [ - "PostSecret" - ], - "uid": "_:PostSecret7" - } - cond: "@if(eq(len(PostSecret7), 0))" - - setjson: | - { - "PostSecret.title": "ps3", - "dgraph.type": [ - "PostSecret" - ], - "uid": "_:PostSecret10" - } - cond: "@if(eq(len(PostSecret10), 0))" - - setjson: | - { - "PostSecret.title": "ps4", - "dgraph.type": [ - "PostSecret" - ], - "uid": "_:PostSecret13" - } - cond: "@if(eq(len(PostSecret13), 0))" - - setjson: | - { - "PostSecret.title": "ps5", - "dgraph.type": [ - "PostSecret" - ], - "uid": "_:PostSecret16" - } - cond: "@if(eq(len(PostSecret16), 0))" - - setjson: | - { - "PostSecret.title": "ps6", - "dgraph.type": [ - "PostSecret" - ], - "uid": "_:PostSecret19" - } - cond: "@if(eq(len(PostSecret19), 0))" - - setjson: | - { - "PostSecret.title": "ps7", - "dgraph.type": [ - "PostSecret" - ], - "uid": "_:PostSecret22" - } - cond: "@if(eq(len(PostSecret22), 0))" - - setjson: | - { - "PostSecret.title": "ps8", - "dgraph.type": [ - "PostSecret" + "Author.name":"A.N. Author", + "Author.posts": + [ + { + "Post.author": + { + "uid":"_:Author9" + }, + "Post.ps": + { + "uid":"0x1" + }, + "Post.title":"post1", + "dgraph.type":["Post"], + "uid":"_:Post10" + }, + { + "Post.author": + { + "uid":"_:Author9" + }, + "Post.ps": + { + "uid":"0x2" + }, + "Post.title":"post2", + "dgraph.type":["Post"], + "uid":"_:Post11" + }, + { + "Post.author": + { + "uid":"_:Author9" + }, + "Post.ps": + { + "uid":"0x3" + }, + "Post.title":"post3", + "dgraph.type":["Post"], + "uid":"_:Post12" + }, + { + "Post.author": + { + "uid":"_:Author9" + }, + "Post.ps": + { + "uid":"0x4" + }, + "Post.title":"post4", + "dgraph.type":["Post"], + "uid":"_:Post13" + }, + { + "Post.author": + { + "uid":"_:Author9" + }, + "Post.ps": + { + "PostSecret.title":"ps5", + "dgraph.type":["PostSecret"], + "uid":"_:PostSecret5" + }, + "Post.title":"post5", + "dgraph.type":["Post"], + "uid":"_:Post14" + }, + { + "Post.author": + { + "uid":"_:Author9" + }, + "Post.ps": + { + "PostSecret.title":"ps6", + "dgraph.type":["PostSecret"], + "uid":"_:PostSecret6" + }, + "Post.title":"post6", + "dgraph.type":["Post"], + "uid":"_:Post15" + }, + { + "Post.author": + { + "uid":"_:Author9" + }, + "Post.ps": + { + "PostSecret.title":"ps7", + "dgraph.type":["PostSecret"], + "uid":"_:PostSecret7" + }, + "Post.title":"post7", + "dgraph.type":["Post"], + "uid":"_:Post16" + }, + { + "Post.author": + { + "uid":"_:Author9" + }, + "Post.ps": + { + "PostSecret.title":"ps8", + "dgraph.type":["PostSecret"], + "uid":"_:PostSecret8" + }, + "Post.title":"post8", + "dgraph.type":["Post"], + "uid":"_:Post17" + } ], - "uid": "_:PostSecret25" - } - cond: "@if(eq(len(PostSecret25), 0))" - dgmutationssec: - - setjson: | - { - "Author.name": "A.N. Author", - "Author.posts": [{ - "Post.author": { - "uid": "_:Author1" - }, - "Post.ps": { - "uid": "uid(PostSecret4)" - }, - "Post.title": "post1", - "dgraph.type": ["Post"], - "uid": "_:Post2" - }, { - "Post.author": { - "uid": "_:Author1" - }, - "Post.ps": { - "uid": "uid(PostSecret7)" - }, - "Post.title": "post2", - "dgraph.type": ["Post"], - "uid": "_:Post5" - }, { - "Post.author": { - "uid": "_:Author1" - }, - "Post.ps": { - "uid": "uid(PostSecret10)" - }, - "Post.title": "post3", - "dgraph.type": ["Post"], - "uid": "_:Post8" - }, { - "Post.author": { - "uid": "_:Author1" - }, - "Post.ps": { - "uid": "uid(PostSecret13)" - }, - "Post.title": "post4", - "dgraph.type": ["Post"], - "uid": "_:Post11" - }, { - "Post.author": { - "uid": "_:Author1" - }, - "Post.ps": { - "uid": "uid(PostSecret16)" - }, - "Post.title": "post5", - "dgraph.type": ["Post"], - "uid": "_:Post14" - }, { - "Post.author": { - "uid": "_:Author1" - }, - "Post.ps": { - "uid": "uid(PostSecret19)" - }, - "Post.title": "post6", - "dgraph.type": ["Post"], - "uid": "_:Post17" - }, { - "Post.author": { - "uid": "_:Author1" - }, - "Post.ps": { - "uid": "uid(PostSecret22)" - }, - "Post.title": "post7", - "dgraph.type": ["Post"], - "uid": "_:Post20" - }, { - "Post.author": { - "uid": "_:Author1" - }, - "Post.ps": { - "uid": "uid(PostSecret25)" - }, - "Post.title": "post8", - "dgraph.type": ["Post"], - "uid": "_:Post23" - }], - "dgraph.type": ["Author"], - "uid": "_:Author1" + "dgraph.type":["Author"], + "uid":"_:Author9" } - cond: "@if(eq(len(PostSecret4), 1) AND eq(len(PostSecret7), 1) AND eq(len(PostSecret10), 1) AND eq(len(PostSecret13), 1) AND eq(len(PostSecret16), 1) AND eq(len(PostSecret19), 1) AND eq(len(PostSecret22), 1) AND eq(len(PostSecret25), 1))" - name: "Add mutation for predicates with special characters having @dgraph directive." @@ -575,20 +516,20 @@ { "name": "A.N. Author", "pwd": "Password" } explanation: "The input and variables should be used for the mutation, with a uid and type getting injected and all data transformed to underlying Dgraph edge names" + dgquery: |- + query { + User1(func: eq(User.name, "A.N. Author")) @filter(type(User)) { + uid + } + } dgmutations: - setjson: | - { "uid":"_:User2", + { + "uid":"_:User1", "dgraph.type":["User"], "User.name":"A.N. Author", "User.pwd":"Password" } - cond: "@if(eq(len(User2), 0))" - dgquery: |- - query { - User2 as User2(func: eq(User.name, "A.N. Author")) @filter(type(User)) { - uid - } - } - name: "Add Multiple Mutations with embedded value" @@ -605,13 +546,15 @@ injected and all data transformed to underlying Dgraph edge names" dgmutations: - setjson: | - { "uid":"_:Author1", + { + "uid":"_:Author1", "dgraph.type":["Author"], "Author.name":"A.N. Author", "Author.posts":[] } - setjson: | - { "uid":"_:Author2", + { + "uid":"_:Author2", "dgraph.type":["Author"], "Author.name":"Different Author", "Author.posts":[] @@ -638,19 +581,55 @@ Dgraph JSON mutation" dgquery: |- query { - Country2 as Country2(func: uid(0x123)) @filter(type(Country)) { + Country1(func: uid(0x123)) @filter(type(Country)) { uid } } + qnametouid: |- + { + "Country1":"0x123" + } dgmutations: - setjson: | - { "uid":"_:Author1", + { + "uid":"_:Author2", "dgraph.type":["Author"], "Author.name":"A.N. Author", - "Author.country": { "uid": "0x123" }, + "Author.country": + { + "uid": "0x123" + }, "Author.posts":[] } - cond: "@if(eq(len(Country2), 1))" + +- + name: "Add mutation with missing reference" + gqlmutation: | + mutation addAuthor($auth: AddAuthorInput!) { + addAuthor(input: [$auth]) { + author { + name + } + } + } + gqlvariables: | + { "auth": + { "name": "A.N. Author", + "country": { "id": "0x123" }, + "posts": [] + } + } + explanation: "This should throw an error as 0x123 is not a valid Country node" + dgquery: |- + query { + Country1(func: uid(0x123)) @filter(type(Country)) { + uid + } + } + error2: + { + "message": "failed to rewrite mutation payload because ID \"0x123\" isn't a Country" + } - name: "Add mutation with invalid reference" @@ -672,7 +651,7 @@ explanation: "A reference must be a valid UID" error: { "message": - "failed to rewrite mutation payload because ID argument (HI!) was not able to be parsed" } + "failed to rewrite mutation payload because ID argument (HI!) was not able to be parsed" } - name: "Add mutation with inverse reference" @@ -695,22 +674,25 @@ a new 'posts' edge." dgquery: |- query { - Author2 as Author2(func: uid(0x2)) @filter(type(Author)) { + Author1(func: uid(0x2)) @filter(type(Author)) { uid } } + qnametouid: |- + { + "Author1": "0x2" + } dgmutations: - setjson: | - { "uid" : "_:Post1", + { "uid" : "_:Post2", "dgraph.type" : ["Post"], "Post.title" : "Exciting post", "Post.text" : "A really good post", "Post.author": { "uid" : "0x2", - "Author.posts" : [ { "uid": "_:Post1" } ] + "Author.posts" : [ { "uid": "_:Post2" } ] } } - cond: "@if(eq(len(Author2), 1))" - name: "Add mutation for a type that implements an interface" @@ -744,7 +726,7 @@ } - - name: "Add mutation using xid code" + name: "Add mutation using xid code 1" gqlmutation: | mutation addState($input: AddStateInput!) { addState(input: [$input]) { @@ -764,25 +746,66 @@ explanation: "The add mutation should get rewritten into a Dgraph upsert mutation" dgquery: |- query { - State2 as State2(func: eq(State.code, "nsw")) @filter(type(State)) { + State1(func: eq(State.code, "nsw")) @filter(type(State)) { uid } - Country3 as Country3(func: uid(0x12)) @filter(type(Country)) { + Country2(func: uid(0x12)) @filter(type(Country)) { uid } } + qnametouid: |- + { + "Country2": "0x12" + } dgmutations: - setjson: | - { "uid" : "_:State2", + { "uid" : "_:State1", "dgraph.type": ["State"], "State.name": "NSW", "State.code": "nsw", "State.country": { "uid": "0x12", - "Country.states": [ { "uid": "_:State2" } ] + "Country.states": [ { "uid": "_:State1" } ] } } - cond: "@if(eq(len(State2), 0) AND eq(len(Country3), 1))" + +- + name: "Add mutation using xid code 2" + explanation: "Error thrown as node with code nsw exists." + gqlmutation: | + mutation addState($input: AddStateInput!) { + addState(input: [$input]) { + state { + name + } + } + } + gqlvariables: | + { "input": + { + "code": "nsw", + "name": "NSW", + "country": { "id": "0x12" } + } + } + dgquery: |- + query { + State1(func: eq(State.code, "nsw")) @filter(type(State)) { + uid + } + Country2(func: uid(0x12)) @filter(type(Country)) { + uid + } + } + qnametouid: |- + { + "State1": "0x11", + "Country2": "0x12" + } + error2: + { + "message": "failed to rewrite mutation payload because id nsw already exists for type State" + } - name: "Add mutation using code on type which also has an ID field" @@ -804,18 +827,18 @@ explanation: "The add mutation should get rewritten into a Dgraph upsert mutation" dgquery: |- query { - Editor2 as Editor2(func: eq(Editor.code, "editor")) @filter(type(Editor)) { + Editor1(func: eq(Editor.code, "editor")) @filter(type(Editor)) { uid } } dgmutations: - setjson: | - { "uid" : "_:Editor2", + { + "uid" : "_:Editor1", "dgraph.type": ["Editor"], "Editor.name": "A.N. Editor", "Editor.code": "editor" } - cond: "@if(eq(len(Editor2), 0))" - name: "Deep add mutation" @@ -954,34 +977,41 @@ } dgquery: |- query { - Post3 as Post3(func: uid(0x123)) @filter(type(Post)) { + Post1(func: uid(0x123)) @filter(type(Post)) { uid } - var(func: uid(Post3)) { + } + qnametouid: |- + { + "Post1":"0x123" + } + dgquerysec: |- + query { + var(func: uid(0x123)) { Author4 as Post.author } } dgmutations: - setjson: | - { "uid": "_:Author1", + { "uid": "_:Author2", "dgraph.type": [ "Author" ], "Author.name": "A.N. Author", "Author.dob": "2000-01-01", "Author.dob": "2000-01-01", "Author.posts": [ { - "uid": "_:Post2", + "uid": "_:Post3", "dgraph.type": [ "Post" ], "Post.title": "New post", "Post.text": "A really new post", "Post.author": { - "uid": "_:Author1" + "uid": "_:Author2" } }, { "uid": "0x123", "Post.author": { - "uid": "_:Author1" + "uid": "_:Author2" } } ] @@ -990,10 +1020,9 @@ [ { "uid": "uid(Author4)", - "Author.posts": [{"uid": "uid(Post3)"}] + "Author.posts": [{"uid": "0x123"}] } ] - cond: "@if(eq(len(Post3), 1))" - name: "Deep add multiple with existing" @@ -1038,39 +1067,47 @@ } dgquery: |- query { - Post3 as Post3(func: uid(0x123)) @filter(type(Post)) { + Post1(func: uid(0x123)) @filter(type(Post)) { uid } - var(func: uid(Post3)) { - Author4 as Post.author - } - Post7 as Post7(func: uid(0x124)) @filter(type(Post)) { + Post2(func: uid(0x124)) @filter(type(Post)) { uid } - var(func: uid(Post7)) { + } + qnametouid: |- + { + "Post1":"0x123", + "Post2":"0x124" + } + dgquerysec: |- + query { + var(func: uid(0x123)) { + Author5 as Post.author + } + var(func: uid(0x124)) { Author8 as Post.author } } dgmutations: - setjson: | - { "uid": "_:Author1", + { "uid": "_:Author3", "dgraph.type": [ "Author" ], "Author.name": "A.N. Author", "Author.dob": "2000-01-01", "Author.posts": [ { - "uid": "_:Post2", + "uid": "_:Post4", "dgraph.type": [ "Post" ], "Post.title": "New post", "Post.text": "A really new post", "Post.author": { - "uid": "_:Author1" + "uid": "_:Author3" } }, { "uid": "0x123", "Post.author": { - "uid": "_:Author1" + "uid": "_:Author3" } } ] @@ -1078,34 +1115,34 @@ deletejson: | [ { - "uid": "uid(Author4)", + "uid": "uid(Author5)", "Author.posts": [ { - "uid": "uid(Post3)" + "uid": "0x123" } ] } ] - cond: "@if(eq(len(Post3), 1))" - setjson: | - { "uid": "_:Author5", + { + "uid": "_:Author6", "dgraph.type": [ "Author" ], "Author.name": "Different Author", "Author.dob": "2000-01-01", "Author.posts": [ { - "uid": "_:Post6", + "uid": "_:Post7", "dgraph.type": [ "Post" ], "Post.title": "New new post", "Post.text": "A wonderful post", "Post.author": { - "uid": "_:Author5" + "uid": "_:Author6" } }, { "uid": "0x124", "Post.author": { - "uid": "_:Author5" + "uid": "_:Author6" } } ] @@ -1116,12 +1153,11 @@ "uid": "uid(Author8)", "Author.posts": [ { - "uid": "uid(Post7)" + "uid": "0x124" } ] } ] - cond: "@if(eq(len(Post7), 1))" - name: "Deep add with two existing" @@ -1151,22 +1187,30 @@ } dgquery: |- query { - Post2 as Post2(func: uid(0x123)) @filter(type(Post)) { + Post1(func: uid(0x123)) @filter(type(Post)) { uid } - var(func: uid(Post2)) { - Author3 as Post.author - } - Post4 as Post4(func: uid(0x456)) @filter(type(Post)) { + Post2(func: uid(0x456)) @filter(type(Post)) { uid } - var(func: uid(Post4)) { + } + qnametouid: |- + { + "Post1":"0x123", + "Post2":"0x456" + } + dgquerysec: |- + query { + var(func: uid(0x123)) { + Author4 as Post.author + } + var(func: uid(0x456)) { Author5 as Post.author } } dgmutations: - setjson: | - { "uid": "_:Author1", + { "uid": "_:Author3", "dgraph.type": [ "Author" ], "Author.name": "A.N. Author", "Author.dob": "2000-01-01", @@ -1174,13 +1218,13 @@ { "uid": "0x123", "Post.author": { - "uid": "_:Author1" + "uid": "_:Author3" } }, { "uid": "0x456", "Post.author": { - "uid": "_:Author1" + "uid": "_:Author3" } } ] @@ -1188,15 +1232,14 @@ deletejson: | [ { - "uid": "uid(Author3)", - "Author.posts": [{"uid": "uid(Post2)"}] + "uid": "uid(Author4)", + "Author.posts": [{"uid": "0x123"}] }, { "uid": "uid(Author5)", - "Author.posts": [{"uid": "uid(Post4)"}] + "Author.posts": [{"uid": "0x456"}] } ] - cond: "@if(eq(len(Post2), 1) AND eq(len(Post4), 1))" - name: "Deep add with null" @@ -1293,7 +1336,7 @@ } - - name: "Add mutation with deep xid choices" + name: "Add mutation with deep xid choices 1" gqlmutation: | mutation addCountry($input: AddCountryInput!) { addCountry(input: [$input]) { @@ -1312,60 +1355,36 @@ } ] } } - explanation: "The add mutation has two options depending on if dg exists" + explanation: "No nodes exist. Both nodes are created." dgquery: |- query { - State3 as State3(func: eq(State.code, "dg")) @filter(type(State)) { + State1(func: eq(State.code, "dg")) @filter(type(State)) { uid } } - dgmutations: - setjson: | { - "State.code": "dg", - "State.name": "Dgraph", - "dgraph.type": [ - "State" - ], - "uid": "_:State3" - } - cond: "@if(eq(len(State3), 0))" - - dgquerysec: |- - query { - State3 as State3(func: eq(State.code, "dg")) @filter(type(State)) { - uid - } - var(func: uid(State3)) { - Country4 as State.country - } - } - - dgmutationssec: - - setjson: | - { - "uid": "_:Country1", - "dgraph.type": ["Country"], - "Country.name": "Dgraph Land", - "Country.states": [ { - "uid": "uid(State3)", - "State.country": { - "uid": "_:Country1" - } - } ] + "Country.name":"Dgraph Land", + "Country.states": + [ + { + "State.code":"dg", + "State.country": + { + "uid":"_:Country2" + }, + "State.name":"Dgraph", + "dgraph.type":["State"], + "uid":"_:State1" + } + ], + "dgraph.type":["Country"], + "uid":"_:Country2" } - deletejson: | - [ - { - "uid": "uid(Country4)", - "Country.states": [{"uid": "uid(State3)"}] - } - ] - cond: "@if(eq(len(State3), 1))" - - name: "Add mutation with deep xid that must be reference" + name: "Add mutation with deep xid choices 2" gqlmutation: | mutation addCountry($input: AddCountryInput!) { addCountry(input: [$input]) { @@ -1379,42 +1398,150 @@ { "name": "Dgraph Land", "states": [ { - "code": "dg" + "code": "dg", + "name": "Dgraph" } ] } } - explanation: "The add mutation has only one option because the state isn't a valid create - because it's missing required field name" + explanation: "The state exists. It is linked to the new Country. Its link to old country is deleted." dgquery: |- query { - State3 as State3(func: eq(State.code, "dg")) @filter(type(State)) { + State1(func: eq(State.code, "dg")) @filter(type(State)) { uid } - var(func: uid(State3)) { - Country4 as State.country - } } - dgmutations: + qnametouid: |- + { + "State1":"0x12" + } + dgquerysec: |- + query { + var(func: uid(0x12)) { + Country3 as State.country + } + } + dgmutations: + - setjson: | + { + "Country.name":"Dgraph Land", + "Country.states": + [ + { + "State.country": + { + "uid":"_:Country2" + }, + "uid":"0x12" + } + ], + "dgraph.type":["Country"], + "uid":"_:Country2" + } + deletejson: | + [ + { + "uid":"uid(Country3)", + "Country.states": + [ + { + "uid":"0x12" + } + ] + } + ] + +- + name: "Add mutation with deep xid that must be reference 1" + gqlmutation: | + mutation addCountry($input: AddCountryInput!) { + addCountry(input: [$input]) { + country { + name + } + } + } + gqlvariables: | + { "input": + { + "name": "Dgraph Land", + "states": [ { + "code": "dg" + } ] + } + } + explanation: "The add mutation has only one option because the state isn't a valid create + because it's missing required field name" + dgquery: |- + query { + State1(func: eq(State.code, "dg")) @filter(type(State)) { + uid + } + } + qnametouid: |- + { + "State1":"0x12" + } + dgquerysec: |- + query { + var(func: uid(0x12)) { + Country3 as State.country + } + } + dgmutations: - setjson: | { - "uid": "_:Country1", + "uid": "_:Country2", "dgraph.type": ["Country"], "Country.name": "Dgraph Land", - "Country.states": [ { - "uid": "uid(State3)", - "State.country": { - "uid": "_:Country1" - } - } ] + "Country.states": + [ + { + "uid": "0x12", + "State.country": + { + "uid": "_:Country2" + } + } + ] } deletejson: | [ { - "uid": "uid(Country4)", - "Country.states": [{"uid": "uid(State3)"}] + "uid": "uid(Country3)", + "Country.states": [{"uid": "0x12"}] } ] - cond: "@if(eq(len(State3), 1))" + +- + name: "Add mutation with deep xid that must be reference 2" + gqlmutation: | + mutation addCountry($input: AddCountryInput!) { + addCountry(input: [$input]) { + country { + name + } + } + } + gqlvariables: | + { "input": + { + "name": "Dgraph Land", + "states": [ { + "code": "dg" + } ] + } + } + explanation: "Error is thrown as State with code dg does not exist" + dgquery: |- + query { + State1(func: eq(State.code, "dg")) @filter(type(State)) { + uid + } + } + error2: + { + "message": "failed to rewrite mutation payload because type State requires a value for field name, but no value present" + } - @@ -1458,23 +1585,26 @@ "directed": [{ "id": "0x2" }] } } - explanation: "The reference to the directed.movies edge node should not add a new movie edge." + explanation: "Movie node exists and is not created" dgquery: |- query { - Movie2 as Movie2(func: uid(0x2)) @filter(type(Movie)) { + Movie1(func: uid(0x2)) @filter(type(Movie)) { uid } } + qnametouid: |- + { + "Movie1":"0x2" + } dgmutations: - setjson: | - { "uid" : "_:MovieDirector1", + { "uid" : "_:MovieDirector2", "dgraph.type" : ["MovieDirector"], "MovieDirector.name" : "Steven Spielberg", "directed.movies": [{ "uid" : "0x2" }] } - cond: "@if(eq(len(Movie2), 1))" - name: "Top Level Duplicate XIDs with same object Test" gqlmutation: | @@ -1554,64 +1684,62 @@ is same or contains just xid, it should not return error." dgquery: |- query { - District3 as District3(func: eq(District.code, "D1")) @filter(type(District)) { - uid - } - } - dgquerysec: |- - query { - District3 as District3(func: eq(District.code, "D1")) @filter(type(District)) { + District1(func: eq(District.code, "D1")) @filter(type(District)) { uid } } dgmutations: - setjson: | { - "District.code": "D1", - "District.name": "Dist1", - "dgraph.type": [ - "District" - ], - "uid": "_:District3" - } - cond: "@if(eq(len(District3), 0))" - - dgmutationssec: - - setjson: | - { - "City.name":"Bengaluru", - "City.district":{ - "District.cities":[{"uid":"_:City1"}], - "uid":"uid(District3)" + "City.district": + { + "District.cities": + [ + { + "uid":"_:City2" + } + ], + "District.code":"D1", + "District.name":"Dist1", + "dgraph.type":["District"], + "uid":"_:District1" }, + "City.name":"Bengaluru", "dgraph.type":["City"], - "uid":"_:City1" + "uid":"_:City2" } - cond: "@if(eq(len(District3), 1))" - setjson: | { - "City.name":"NY", - "City.district":{ - "District.cities":[{"uid":"_:City4"}], - "uid":"uid(District3)" + "City.district": + { + "District.cities": + [ + { + "uid":"_:City3" + } + ], + "uid":"_:District1" }, + "City.name":"NY", "dgraph.type":["City"], - "uid":"_:City4" + "uid":"_:City3" } - cond: "@if(eq(len(District3), 1))" - setjson: | { - "City.name":"Sydney", - "City.district":{ - "District.cities":[{"uid":"_:City6"}], - "uid":"uid(District3)" + "City.district": + { + "District.cities": + [ + { + "uid":"_:City4" + } + ], + "uid":"_:District1" }, + "City.name":"Sydney", "dgraph.type":["City"], - "uid":"_:City6" + "uid":"_:City4" } - cond: "@if(eq(len(District3), 1))" - - - name: "Deep Mutation Duplicate XIDs with same object with @hasInverse Test" gqlmutation: | @@ -1820,22 +1948,38 @@ "posts": [ { "postID": "0x456" }, {"title": "New Post", "author": {"name": "Abhimanyu"}} ] } } + dgquery: |- + query { + Post1(func: uid(0x456)) @filter(type(Post)) { + uid + } + } + qnametouid: |- + { + "Post1": "0x456" + } + dgquerysec: |- + query { + var(func: uid(0x456)) { + Author3 as Post.author + } + } dgmutations: - setjson: | { - "uid":"_:Author1", + "uid":"_:Author2", "dgraph.type":["Author"], "Author.name":"A.N. Author", "Author.posts": [ { "uid": "0x456", - "Post.author": { "uid": "_:Author1" } + "Post.author": { "uid": "_:Author2" } }, { "uid": "_:Post4", "dgraph.type": ["Post"], "Post.title": "New Post", - "Post.author": { "uid": "_:Author1" } + "Post.author": { "uid": "_:Author2" } } ] } @@ -1843,21 +1987,12 @@ [ { "uid": "uid(Author3)", - "Author.posts": [ { "uid": "uid(Post2)" } ] + "Author.posts": [ { "uid": "0x456" } ] } ] - cond: "@if(eq(len(Post2), 1))" - dgquery: |- - query { - Post2 as Post2(func: uid(0x456)) @filter(type(Post)) { - uid - } - var(func: uid(Post2)) { - Author3 as Post.author - } - } - name: "Additional Deletes - Add connects to existing node by XID" + explanation: "One of the states exists. Country attached to that state is deleted." gqlmutation: | mutation addCountry($inp: AddCountryInput!) { addCountry(input: [$inp]) { @@ -1876,84 +2011,65 @@ ] } } - dgquery: |- query { - State3 as State3(func: eq(State.code, "abc")) @filter(type(State)) { + State1(func: eq(State.code, "abc")) @filter(type(State)) { uid } - State6 as State6(func: eq(State.code, "def")) @filter(type(State)) { + State2(func: eq(State.code, "def")) @filter(type(State)) { uid } } - dgmutations: - - setjson: | - { - "State.code": "abc", - "State.name": "Alphabet", - "dgraph.type": [ - "State" - ], - "uid": "_:State3" - } - cond: "@if(eq(len(State3), 0))" - - setjson: | - { - "State.code": "def", - "State.name": "Vowel", - "dgraph.type": [ - "State" - ], - "uid": "_:State6" - } - cond: "@if(eq(len(State6), 0))" - + qnametouid: |- + { + "State1": "0x1234" + } dgquerysec: |- query { - State3 as State3(func: eq(State.code, "abc")) @filter(type(State)) { - uid - } - var(func: uid(State3)) { + var(func: uid(0x1234)) { Country4 as State.country } - State6 as State6(func: eq(State.code, "def")) @filter(type(State)) { - uid - } - var(func: uid(State6)) { - Country7 as State.country - } } - dgmutationssec: + dgmutations: - setjson: | { - "uid" : "_:Country1", - "dgraph.type": ["Country"], - "Country.name": "A Country", - "Country.states": [ - { - "uid": "uid(State3)", - "State.country": { "uid": "_:Country1" } - }, - { - "uid": "uid(State6)", - "State.country": { "uid": "_:Country1" } - } - ] + "Country.name":"A Country", + "Country.states": + [ + { + "State.country": + { + "uid":"_:Country3" + }, + "uid":"0x1234" + }, + { + "State.code":"def", + "State.country": + { + "uid":"_:Country3" + }, + "State.name":"Vowel", + "dgraph.type":["State"], + "uid":"_:State2" + } + ], + "dgraph.type":["Country"], + "uid":"_:Country3" } deletejson: | [ { - "uid": "uid(Country4)", - "Country.states": [ { "uid": "uid(State3)" } ] - }, - { - "uid": "uid(Country7)", - "Country.states": [ { "uid": "uid(State6)" } ] + "uid":"uid(Country4)", + "Country.states": + [ + {"uid":"0x1234"} + ] } ] - cond: "@if(eq(len(State3), 1) AND eq(len(State6), 1))" -- name: "Deep XID 4 level deep" +- name: "Deep XID 4 level deep 1" + explanation: "No nodes exist. All nodes are created." gqlmutation: | mutation addStudent($student: AddStudentInput!) { addStudent(input: [$student]) { @@ -1983,115 +2099,69 @@ } dgquery: |- query { - Teacher4 as Teacher4(func: eq(People.xid, "T0")) @filter(type(Teacher)) { + Student1(func: eq(People.xid, "S0")) @filter(type(Student)) { + uid + } + Teacher2(func: eq(People.xid, "T0")) @filter(type(Teacher)) { uid } - Teacher8 as Teacher8(func: eq(People.xid, "T1")) @filter(type(Teacher)) { + Student3(func: eq(People.xid, "S1")) @filter(type(Student)) { uid } - Student6 as Student6(func: eq(People.xid, "S1")) @filter(type(Student)) { + Teacher4(func: eq(People.xid, "T1")) @filter(type(Teacher)) { uid } } - dgmutations: - setjson: | { + "People.name":"Student0", + "People.xid":"S0", + "Student.taughtBy": + [ + { "People.name":"teacher0", "People.xid":"T0", + "Teacher.teaches": + [ + { + "uid":"_:Student1" + }, + { + "People.name":"Student1", + "People.xid":"S1", + "Student.taughtBy": + [ + { + "uid":"_:Teacher2" + }, + { + "People.name":"teacher1", + "People.xid":"T1", + "Teacher.teaches": + [ + { + "uid":"_:Student3" + } + ], + "dgraph.type":["Teacher","People"], + "uid":"_:Teacher4" + } + ], + "dgraph.type":["Student","People"], + "uid":"_:Student3" + } + ], "dgraph.type":["Teacher","People"], - "uid":"_:Teacher4" - } - - cond: "@if(eq(len(Teacher4), 0))" - - - setjson: | - { - "People.name": "Student1", - "People.xid": "S1", - "Student.taughtBy": [{"uid": "_:Teacher4"}], - "dgraph.type": ["Student","People"], - "uid": "_:Student6" - } - cond: "@if(eq(len(Student6), 0) AND eq(len(Teacher4), 0))" - - - setjson: | - { - "People.name": "teacher1", - "People.xid": "T1", - "Teacher.teaches": [{"uid": "_:Student6"}], - "dgraph.type": ["Teacher","People"], - "uid": "_:Teacher8" - } - cond: "@if(eq(len(Teacher8), 0) AND eq(len(Student6), 0) AND eq(len(Teacher4), 0))" - - - setjson: | - { - "Student.taughtBy": [{"uid": "uid(Teacher8)"}], - "uid": "_:Student6" - } - cond: "@if(eq(len(Teacher8), 1) AND eq(len(Student6), 0) AND eq(len(Teacher4), 0))" - - - setjson: | - { - "Student.taughtBy": [{"uid": "_:Teacher8"}], - "uid": "_:Student6" - } - cond: "@if(eq(len(Teacher8), 0) AND eq(len(Student6), 0) AND eq(len(Teacher4), 0))" - - - setjson: | - { - "Teacher.teaches": [{"uid": "_:Student6"}], - "uid": "uid(Teacher8)" - } - cond: "@if(eq(len(Teacher8), 1) AND eq(len(Student6), 0) AND eq(len(Teacher4), 0))" - - - setjson: | - { - "Teacher.teaches": [{"uid": "uid(Student6)"}], - "uid": "_:Teacher4" - } - cond: "@if(eq(len(Student6), 1) AND eq(len(Teacher4), 0))" - - - setjson: | - { - "Teacher.teaches": [{"uid": "_:Student6"}], - "uid": "_:Teacher4" - } - cond: "@if(eq(len(Student6), 0) AND eq(len(Teacher4), 0))" - - - setjson: | - { - "Student.taughtBy": [{"uid": "_:Teacher4"}], - "uid": "uid(Student6)" + "uid":"_:Teacher2" + } + ], + "dgraph.type":["Student","People"], + "uid":"_:Student1" } - cond: "@if(eq(len(Student6), 1) AND eq(len(Teacher4), 0))" - - dgquerysec: |- - query { - Student2 as Student2(func: eq(People.xid, "S0")) @filter(type(Student)) { - uid - } - Teacher4 as Teacher4(func: eq(People.xid, "T0")) @filter(type(Teacher)) { - uid - } - } - dgmutationssec: - - setjson: | - { - "People.name": "Student0", - "People.xid": "S0", - "Student.taughtBy": [{ - "Teacher.teaches": [{"uid": "_:Student2"}], - "uid": "uid(Teacher4)" - }], - "dgraph.type": ["Student","People"], - "uid": "_:Student2" - } - cond: "@if(eq(len(Student2), 0) AND eq(len(Teacher4), 1))" - -- name: "Deep XID Add top level hasInverse" +- name: "Deep XID 4 level deep 2" + explanation: "Teacher T1 also teaches the newly added student at top level, S0." gqlmutation: | mutation addStudent($student: AddStudentInput!) { addStudent(input: [$student]) { @@ -2110,89 +2180,303 @@ "name": "teacher0", "teaches": [{ "xid": "S1", - "name": "Student1" + "name": "Student1", + "taughtBy": [{ + "xid": "T1", + "name": "teacher1", + "teaches": [{ + "xid": "S0" + }] + }] }] }] } } dgquery: |- query { - Teacher4 as Teacher4(func: eq(People.xid, "T0")) @filter(type(Teacher)) { + Student1(func: eq(People.xid, "S0")) @filter(type(Student)) { + uid + } + Teacher2(func: eq(People.xid, "T0")) @filter(type(Teacher)) { + uid + } + Student3(func: eq(People.xid, "S1")) @filter(type(Student)) { uid } - Student6 as Student6(func: eq(People.xid, "S1")) @filter(type(Student)) { + Teacher4(func: eq(People.xid, "T1")) @filter(type(Teacher)) { uid } } - dgmutations: - setjson: | { + "People.name":"Student0", + "People.xid":"S0", + "Student.taughtBy": + [ + { "People.name":"teacher0", "People.xid":"T0", + "Teacher.teaches": + [ + { + "uid":"_:Student1" + }, + { + "People.name":"Student1", + "People.xid":"S1", + "Student.taughtBy": + [ + { + "uid":"_:Teacher2" + }, + { + "People.name":"teacher1", + "People.xid":"T1", + "Teacher.teaches": + [ + { + "uid":"_:Student3" + }, + { + "uid":"_:Student1", + "Student.taughtBy": + [ + { + "uid":"_:Teacher4" + } + ] + } + ], + "dgraph.type":["Teacher","People"], + "uid":"_:Teacher4" + } + ], + "dgraph.type":["Student","People"], + "uid":"_:Student3" + } + ], "dgraph.type":["Teacher","People"], - "uid":"_:Teacher4" - } - - cond: "@if(eq(len(Teacher4), 0))" - - - setjson: | - { - "People.name":"Student1", - "People.xid":"S1", - "Student.taughtBy":[{"uid":"_:Teacher4"}], - "dgraph.type":["Student","People"], - "uid":"_:Student6" - } - cond: "@if(eq(len(Student6), 0) AND eq(len(Teacher4), 0))" - - - setjson: | - { - "Teacher.teaches":[{"uid":"uid(Student6)"}], - "uid":"_:Teacher4" + "uid":"_:Teacher2" + } + ], + "dgraph.type":["Student","People"], + "uid":"_:Student1" } - deletejson: | - cond: "@if(eq(len(Student6), 1) AND eq(len(Teacher4), 0))" - - setjson: | - { - "Teacher.teaches":[{"uid":"_:Student6"}], - "uid":"_:Teacher4" +- name: "Deep XID Add top level hasInverse 1" + explanation: "No nodes exists. All are created." + gqlmutation: | + mutation addStudent($student: AddStudentInput!) { + addStudent(input: [$student]) { + student { + name } - deletejson: | - cond: "@if(eq(len(Student6), 0) AND eq(len(Teacher4), 0))" + } + } + gqlvariables: | + { + "student": { + "xid": "S0", + "name": "Student0", + "taughtBy": [{ + "xid": "T0", + "name": "teacher0", + "teaches": [{ + "xid": "S1", + "name": "Student1" + }] + }] + } + } + dgquery: |- + query { + Student1(func: eq(People.xid, "S0")) @filter(type(Student)) { + uid + } + Teacher2(func: eq(People.xid, "T0")) @filter(type(Teacher)) { + uid + } + Student3(func: eq(People.xid, "S1")) @filter(type(Student)) { + uid + } + } + dgmutations: - setjson: | { - "Student.taughtBy":[{"uid":"_:Teacher4"}], - "uid":"uid(Student6)" + "People.name":"Student0", + "People.xid":"S0", + "Student.taughtBy": + [ + { + "People.name":"teacher0", + "People.xid":"T0", + "Teacher.teaches": + [ + { + "uid":"_:Student1" + }, + { + "People.name":"Student1", + "People.xid":"S1", + "Student.taughtBy": + [ + { + "uid":"_:Teacher2" + } + ], + "dgraph.type":["Student","People"], + "uid":"_:Student3" + } + ], + "dgraph.type":["Teacher","People"], + "uid":"_:Teacher2" + } + ], + "dgraph.type":["Student","People"], + "uid":"_:Student1" } - deletejson: | - cond: "@if(eq(len(Student6), 1) AND eq(len(Teacher4), 0))" - dgquerysec: |- +- name: "Deep XID Add top level hasInverse 2" + explanation: "Teacher T0 exists and is linked to Student S0" + gqlmutation: | + mutation addStudent($student: AddStudentInput!) { + addStudent(input: [$student]) { + student { + name + } + } + } + gqlvariables: | + { + "student": { + "xid": "S0", + "name": "Student0", + "taughtBy": [{ + "xid": "T0", + "name": "teacher0", + "teaches": [{ + "xid": "S1", + "name": "Student1" + }] + }] + } + } + dgquery: |- query { - Student2 as Student2(func: eq(People.xid, "S0")) @filter(type(Student)) { + Student1(func: eq(People.xid, "S0")) @filter(type(Student)) { uid } - Teacher4 as Teacher4(func: eq(People.xid, "T0")) @filter(type(Teacher)) { + Teacher2(func: eq(People.xid, "T0")) @filter(type(Teacher)) { uid } + Student3(func: eq(People.xid, "S1")) @filter(type(Student)) { + uid + } + } + qnametouid: | + { + "Teacher2": "0x987" } + dgmutations: + - setjson: | + { + "People.name":"Student0", + "People.xid":"S0", + "Student.taughtBy": + [ + { + "Teacher.teaches": + [ + { + "uid":"_:Student1" + } + ], + "uid":"0x987" + } + ], + "dgraph.type":["Student","People"], + "uid":"_:Student1" + } - dgmutationssec: +- name: "Deep XID Add top level hasInverse 3" + explanation: "Student S1 exists and is linked to Teacher T0." + gqlmutation: | + mutation addStudent($student: AddStudentInput!) { + addStudent(input: [$student]) { + student { + name + } + } + } + gqlvariables: | + { + "student": { + "xid": "S0", + "name": "Student0", + "taughtBy": [{ + "xid": "T0", + "name": "teacher0", + "teaches": [{ + "xid": "S1", + "name": "Student1" + }] + }] + } + } + dgquery: |- + query { + Student1(func: eq(People.xid, "S0")) @filter(type(Student)) { + uid + } + Teacher2(func: eq(People.xid, "T0")) @filter(type(Teacher)) { + uid + } + Student3(func: eq(People.xid, "S1")) @filter(type(Student)) { + uid + } + } + qnametouid: | + { + "Student3": "0x123" + } + dgmutations: - setjson: | { - "People.name":"Student0", - "People.xid":"S0", - "Student.taughtBy":[{"Teacher.teaches":[{"uid":"_:Student2"}],"uid":"uid(Teacher4)"}], - "dgraph.type":["Student","People"], - "uid":"_:Student2" + "People.name":"Student0", + "People.xid":"S0", + "Student.taughtBy": + [ + { + "People.name":"teacher0", + "People.xid":"T0", + "Teacher.teaches": + [ + { + "uid":"_:Student1" + }, + { + "Student.taughtBy": + [ + { + "uid":"_:Teacher2" + } + ], + "uid":"0x123" + } + ], + "dgraph.type":["Teacher","People"], + "uid":"_:Teacher2" + } + ], + "dgraph.type":["Student","People"], + "uid":"_:Student1" } - cond: "@if(eq(len(Student2), 0) AND eq(len(Teacher4), 1))" -- name: "Deep XID Add lower level hasInvsere" +- name: "Deep XID Add lower level hasInvsere 1" + explanation: "None of the nodes exists. All of them are created." gqlmutation: | mutation addLab($lab: AddLabInput!) { addLab(input: [$lab]) { @@ -2213,95 +2497,176 @@ }] } } + dgquery: |- + query { + Lab1(func: eq(Lab.name, "Lab1")) @filter(type(Lab)) { + uid + } + Computer2(func: eq(Computer.name, "computer1")) @filter(type(Computer)) { + uid + } + ComputerOwner3(func: eq(ComputerOwner.name, "owner1")) @filter(type(ComputerOwner)) { + uid + } + } dgmutations: - setjson: | { - "Computer.name": "computer1", - "dgraph.type": [ - "Computer" + "Lab.computers": + [ + { + "Computer.name":"computer1", + "Computer.owners": + [ + { + "ComputerOwner.computers": + { + "uid":"_:Computer2" + }, + "ComputerOwner.name":"owner1", + "dgraph.type":["ComputerOwner"], + "uid":"_:ComputerOwner3" + } + ], + "dgraph.type":["Computer"], + "uid":"_:Computer2" + } ], - "uid": "_:Computer4" - } - cond: "@if(eq(len(Computer4), 0))" - - - setjson: | - { - "ComputerOwner.computers":{"uid":"_:Computer4"}, - "ComputerOwner.name":"owner1", - "dgraph.type":["ComputerOwner"], - "uid":"_:ComputerOwner6" - } - cond: "@if(eq(len(ComputerOwner6), 0) AND eq(len(Computer4), 0))" - - - setjson: | - { - "Computer.owners":[{"uid":"uid(ComputerOwner6)"}], - "uid":"_:Computer4" - } - cond: "@if(eq(len(ComputerOwner6), 1) AND eq(len(Computer4), 0))" - - - setjson: | - { - "Computer.owners":[{"uid":"_:ComputerOwner6"}], - "uid":"_:Computer4" + "Lab.name":"Lab1", + "dgraph.type":["Lab"], + "uid":"_:Lab1" } - cond: "@if(eq(len(ComputerOwner6), 0) AND eq(len(Computer4), 0))" - - setjson: | - { - "ComputerOwner.computers": { - "uid": "_:Computer4" - }, - "uid": "uid(ComputerOwner6)" +- name: "Deep XID Add lower level hasInvsere 2" + explanation: "computer exists. Computer node is linked to Lab." + gqlmutation: | + mutation addLab($lab: AddLabInput!) { + addLab(input: [$lab]) { + lab { + name } - deletejson: |- - [{ - "Computer.owners": [ - { - "uid": "uid(ComputerOwner6)" - } - ], - "uid": "uid(Computer7)" + } + } + gqlvariables: | + { + "lab": { + "name": "Lab1", + "computers": [{ + "name": "computer1", + "owners": [{ + "name": "owner1" + }] }] - cond: "@if(eq(len(ComputerOwner6), 1) AND eq(len(Computer4), 0))" - - - dgmutationssec: - - setjson: |- - { - "Lab.computers": [ - { - "uid": "uid(Computer4)" - } - ], - "Lab.name": "Lab1", - "dgraph.type": ["Lab"], - "uid": "_:Lab2" - } - cond: "@if(eq(len(Lab2), 0) AND eq(len(Computer4), 1))" - - dgquerysec: |- + } + } + dgquery: |- query { - Lab2 as Lab2(func: eq(Lab.name, "Lab1")) @filter(type(Lab)) { + Lab1(func: eq(Lab.name, "Lab1")) @filter(type(Lab)) { uid } - Computer4 as Computer4(func: eq(Computer.name, "computer1")) @filter(type(Computer)) { + Computer2(func: eq(Computer.name, "computer1")) @filter(type(Computer)) { uid } + ComputerOwner3(func: eq(ComputerOwner.name, "owner1")) @filter(type(ComputerOwner)) { + uid + } + } + qnametouid: | + { + "Computer2": "0x234" } + dgmutations: + - setjson: | + { + "Lab.computers": + [ + { + "uid":"0x234" + } + ], + "Lab.name":"Lab1", + "dgraph.type":["Lab"], + "uid":"_:Lab1" + } +- name: "Deep XID Add lower level hasInvsere 3" + explanation: "Computer Owner exists and is linked to computer." + gqlmutation: | + mutation addLab($lab: AddLabInput!) { + addLab(input: [$lab]) { + lab { + name + } + } + } + gqlvariables: | + { + "lab": { + "name": "Lab1", + "computers": [{ + "name": "computer1", + "owners": [{ + "name": "owner1" + }] + }] + } + } dgquery: |- query { - Computer4 as Computer4(func: eq(Computer.name, "computer1")) @filter(type(Computer)) { + Lab1(func: eq(Lab.name, "Lab1")) @filter(type(Lab)) { uid } - ComputerOwner6 as ComputerOwner6(func: eq(ComputerOwner.name, "owner1")) @filter(type(ComputerOwner)) { + Computer2(func: eq(Computer.name, "computer1")) @filter(type(Computer)) { uid } - var(func: uid(ComputerOwner6)) { - Computer7 as ComputerOwner.computers + ComputerOwner3(func: eq(ComputerOwner.name, "owner1")) @filter(type(ComputerOwner)) { + uid + } + } + qnametouid: | + { + "ComputerOwner3": "0x123" + } + dgquerysec: |- + query { + var(func: uid(0x123)) { + Computer4 as ComputerOwner.computers } } + dgmutations: + - setjson: | + { + "Lab.computers": + [ + { + "Computer.name":"computer1", + "Computer.owners": + [ + { + "ComputerOwner.computers": + { + "uid":"_:Computer2" + }, + "uid":"0x123" + } + ], + "dgraph.type":["Computer"], + "uid":"_:Computer2" + } + ], + "Lab.name":"Lab1", + "dgraph.type":["Lab"], + "uid":"_:Lab1" + } + deletejson: |- + [{ + "Computer.owners": [ + { + "uid": "0x123" + } + ], + "uid": "uid(Computer4)" + }] - name: "Deep mutation alternate id xid" gqlmutation: | @@ -2336,62 +2701,99 @@ } dgquery: |- query { - District3 as District3(func: eq(District.code, "d1")) @filter(type(District)) { + District1(func: eq(District.code, "d1")) @filter(type(District)) { uid } } dgmutations: - setjson: | { - "District.cities": [ - { - "City.district": { - "uid": "_:District3" - }, - "City.name": "c2", - "dgraph.type": [ - "City" - ], - "uid": "_:City4" - } - ], - "District.code": "d1", - "District.name": "d1", - "dgraph.type": [ - "District" - ], - "uid": "_:District3" + "City.district": + { + "District.cities": + [ + { + "uid":"_:City2" + }, + { + "City.district": + { + "uid":"_:District1" + }, + "City.name":"c2", + "dgraph.type":["City"], + "uid":"_:City3" + } + ], + "District.code":"d1", + "District.name":"d1", + "dgraph.type":["District"], + "uid":"_:District1" + }, + "City.name":"c1", + "dgraph.type":["City"], + "uid":"_:City2" } - cond: "@if(eq(len(District3), 0))" - - dgquerysec: |- +- name: "Deep mutation alternate id xid with existing XID" + gqlmutation: | + mutation addAuthor($city: AddCityInput!) { + addCity(input: [$city]) { + city { + name + district { + code + name + cities { + name + district { + code + name + } + } + } + } + } + } + gqlvariables: | + { + "city": { + "name": "c1", + "district":{ + "name":"d1", + "code":"d1", + "cities":[{"name": "c2"}] + } + } + } + dgquery: |- query { - District3 as District3(func: eq(District.code, "d1")) @filter(type(District)) { + District1(func: eq(District.code, "d1")) @filter(type(District)) { uid } } - - dgmutationssec: + qnametouid: |- + { + "District1": "0x123" + } + dgmutations: - setjson: | { - "City.district": { - "District.cities": [ - { - "uid": "_:City1" - } + "City.district": + { + "District.cities": + [ + { + "uid":"_:City2" + } ], - "uid": "uid(District3)" + "uid":"0x123" }, - "City.name": "c1", - "dgraph.type": [ - "City" - ], - "uid": "_:City1" + "City.name":"c1", + "dgraph.type":["City"], + "uid":"_:City2" } - cond: "@if(eq(len(District3), 1))" - - name: "Additional Deletes - deep mutation" gqlmutation: | @@ -2412,62 +2814,137 @@ } } } + dgquery: |- + query { + State1(func: eq(State.code, "abc")) @filter(type(State)) { + uid + } + } dgmutations: - setjson: | { - "State.code": "abc", - "State.name": "Alphabet", - "dgraph.type": [ - "State" - ], - "uid": "_:State4" + "Author.country": + { + "Country.name":"A Country", + "Country.states": + [ + { + "State.code":"abc", + "State.country": {"uid":"_:Country3"}, + "State.name":"Alphabet", + "dgraph.type":["State"], + "uid":"_:State1" + } + ], + "dgraph.type":["Country"], + "uid":"_:Country3" + }, + "Author.name":"A.N. Author", + "dgraph.type":["Author"], + "uid":"_:Author2" } - cond: "@if(eq(len(State4), 0))" - dgmutationssec: - - setjson: | - { - "uid":"_:Author1", - "dgraph.type":["Author"], - "Author.name":"A.N. Author", - "Author.country": { - "uid" : "_:Country2", - "dgraph.type": ["Country"], - "Country.name": "A Country", - "Country.states": [ - { - "uid": "uid(State4)", - "State.country": { "uid": "_:Country2" } - } - ] +- name: "Deep mutation three level xid with no initial XID" + gqlmutation: | + mutation($auth: [AddPost1Input!]!) { + addPost1(input: $auth) { + post1 { + id + comments { + id + replies { + id + } } } - deletejson: | - [ - { - "uid": "uid(Country5)", - "Country.states": [ { "uid": "uid(State4)" } ] - } - ] - cond: "@if(eq(len(State4), 1))" + } + } - dgquerysec: |- + gqlvariables: | + { + "auth": [{ + "id": "post1", + "comments": [{ + "id": "comment1", + "replies": [{ + "id": "reply1" + }] + }] + }, + { + "id": "post2", + "comments": [{ + "id": "comment2", + "replies": [{ + "id": "reply1" + }] + }] + }] + } + dgquery: |- query { - State4 as State4(func: eq(State.code, "abc")) @filter(type(State)) { + Post11(func: eq(Post1.id, "post1")) @filter(type(Post1)) { uid } - var(func: uid(State4)) { - Country5 as State.country + Comment12(func: eq(Comment1.id, "comment1")) @filter(type(Comment1)) { + uid } - } - dgquery: |- - query { - State4 as State4(func: eq(State.code, "abc")) @filter(type(State)) { + Comment13(func: eq(Comment1.id, "reply1")) @filter(type(Comment1)) { + uid + } + Post14(func: eq(Post1.id, "post2")) @filter(type(Post1)) { + uid + } + Comment15(func: eq(Comment1.id, "comment2")) @filter(type(Comment1)) { uid } } + dgmutations: + - setjson: | + { + "Post1.comments": + [ + { + "Comment1.id": "comment1", + "Comment1.replies": + [ + { + "Comment1.id":"reply1", + "dgraph.type": ["Comment1"], + "uid":"_:Comment13" + } + ], + "dgraph.type":["Comment1"], + "uid":"_:Comment12" + } + ], + "Post1.id":"post1", + "dgraph.type":["Post1"], + "uid":"_:Post11" + } + - setjson: | + { + "Post1.comments": + [ + { + "Comment1.id":"comment2", + "Comment1.replies": + [ + { + "uid":"_:Comment13" + } + ], + "dgraph.type":["Comment1"], + "uid":"_:Comment15" + } + ], + "Post1.id":"post2", + "dgraph.type":["Post1"], + "uid":"_:Post14" + } -- name: "Deep mutation three level xid" +- name: "Deep mutation three level xid with existing XIDs 1" + explanation: "reply1 and comment1 exists and is not created" gqlmutation: | mutation($auth: [AddPost1Input!]!) { addPost1(input: $auth) { @@ -2506,120 +2983,147 @@ } dgquery: |- query { - Comment14 as Comment14(func: eq(Comment1.id, "comment1")) @filter(type(Comment1)) { + Post11(func: eq(Post1.id, "post1")) @filter(type(Post1)) { + uid + } + Comment12(func: eq(Comment1.id, "comment1")) @filter(type(Comment1)) { + uid + } + Comment13(func: eq(Comment1.id, "reply1")) @filter(type(Comment1)) { uid } - Comment16 as Comment16(func: eq(Comment1.id, "reply1")) @filter(type(Comment1)) { + Post14(func: eq(Post1.id, "post2")) @filter(type(Post1)) { uid } - Comment110 as Comment110(func: eq(Comment1.id, "comment2")) @filter(type(Comment1)) { + Comment15(func: eq(Comment1.id, "comment2")) @filter(type(Comment1)) { uid } } + qnametouid: | + { + "Comment12": "0x110", + "Comment13": "0x111" + } dgmutations: - # Create comment1 if it doesn't exist. - setjson: | { - "Comment1.id": "comment1", - "dgraph.type": [ - "Comment1" - ], - "uid": "_:Comment14" - } - cond: "@if(eq(len(Comment14), 0))" - # Create reply1 if it doesn't exist. - - setjson: | - { - "Comment1.id": "reply1", - "dgraph.type": [ - "Comment1" - ], - "uid": "_:Comment16" + "Post1.comments": + [ + { + "uid":"0x110" + } + ], + "Post1.id":"post1", + "dgraph.type":["Post1"], + "uid":"_:Post11" } - cond: "@if(eq(len(Comment16), 0) AND eq(len(Comment14), 0))" - # Link comment1 and reply1 if reply exists but comment doesn't. - - setjson: | - {"uid":"_:Comment14", "Comment1.replies": [{"uid": "uid(Comment16)"}]} - cond: "@if(eq(len(Comment16), 1) AND eq(len(Comment14), 0))" - # Link comment1 and reply1 if both don't exist. - - setjson: | - {"uid":"_:Comment14", "Comment1.replies": [{"uid": "_:Comment16"}]} - cond: "@if(eq(len(Comment16), 0) AND eq(len(Comment14), 0))" - # useless mutation which needs investigation - - setjson: | - {"uid":"uid(Comment16)"} - cond: "@if(eq(len(Comment16), 1) AND eq(len(Comment14), 0))" - # create comment2 if it doesn't exist - setjson: | { - "Comment1.id": "comment2", - "dgraph.type": [ - "Comment1" - ], - "uid": "_:Comment110" + "Post1.comments": + [ + { + "Comment1.id":"comment2", + "Comment1.replies": + [ + { + "uid":"0x111" + } + ], + "dgraph.type":["Comment1"], + "uid":"_:Comment15" + } + ], + "Post1.id":"post2", + "dgraph.type":["Post1"], + "uid":"_:Post14" } - cond: "@if(eq(len(Comment110), 0))" - # Create reply1 if it doesn't exist. - - setjson: | - { - "Comment1.id": "reply1", - "dgraph.type": [ - "Comment1" - ], - "uid": "_:Comment16" + +- name: "Deep mutation three level xid with existing XIDs 2" + explanation: "comment2 and comment1 exists. reply1 does not exist. reply1 is not created as its parent exists." + gqlmutation: | + mutation($auth: [AddPost1Input!]!) { + addPost1(input: $auth) { + post1 { + id + comments { + id + replies { + id + } + } } - cond: "@if(eq(len(Comment16), 0) AND eq(len(Comment110), 0))" - # Link comment2 and reply1 if reply exists but comment doesn't. - - setjson: | - {"uid":"_:Comment110", "Comment1.replies": [{"uid": "uid(Comment16)"}]} - cond: "@if(eq(len(Comment16), 1) AND eq(len(Comment110), 0))" - # Link comment2 and reply1 if both don't exist. - - setjson: | - {"uid":"_:Comment110", "Comment1.replies": [{"uid": "_:Comment16"}]} - cond: "@if(eq(len(Comment16), 0) AND eq(len(Comment110), 0))" - # useless mutation which needs investigation + } + } - - setjson: | - {"uid":"uid(Comment16)"} - cond: "@if(eq(len(Comment16), 1) AND eq(len(Comment110), 0))" - dgquerysec: |- + gqlvariables: | + { + "auth": [{ + "id": "post1", + "comments": [{ + "id": "comment1", + "replies": [{ + "id": "reply1" + }] + }] + }, + { + "id": "post2", + "comments": [{ + "id": "comment2", + "replies": [{ + "id": "reply1" + }] + }] + }] + } + dgquery: |- query { - Post12 as Post12(func: eq(Post1.id, "post1")) @filter(type(Post1)) { + Post11(func: eq(Post1.id, "post1")) @filter(type(Post1)) { + uid + } + Comment12(func: eq(Comment1.id, "comment1")) @filter(type(Comment1)) { uid } - Comment14 as Comment14(func: eq(Comment1.id, "comment1")) @filter(type(Comment1)) { + Comment13(func: eq(Comment1.id, "reply1")) @filter(type(Comment1)) { uid } - Post18 as Post18(func: eq(Post1.id, "post2")) @filter(type(Post1)) { + Post14(func: eq(Post1.id, "post2")) @filter(type(Post1)) { uid } - Comment110 as Comment110(func: eq(Comment1.id, "comment2")) @filter(type(Comment1)) { + Comment15(func: eq(Comment1.id, "comment2")) @filter(type(Comment1)) { uid } } - dgmutationssec: + qnametouid: | + { + "Comment12": "0x110", + "Comment15": "0x111" + } + dgmutations: - setjson: | { "Post1.comments": - [{ - "uid":"uid(Comment14)" - }], + [ + { + "uid":"0x110" + } + ], "Post1.id":"post1", "dgraph.type":["Post1"], - "uid":"_:Post12" + "uid":"_:Post11" } - cond: "@if(eq(len(Post12), 0) AND eq(len(Comment14), 1))" - setjson: | { "Post1.comments": - [{ - "uid":"uid(Comment110)" - }], + [ + { + "uid":"0x111" + } + ], "Post1.id":"post2", "dgraph.type":["Post1"], - "uid":"_:Post18" + "uid":"_:Post14" } - cond: "@if(eq(len(Post18), 0) AND eq(len(Comment110), 1))" - name: "Add mutation error on @id field for empty value" @@ -2753,9 +3257,13 @@ } ] } + qnametouid: |- + { + "Parrot1" : "0x123" + } dgquery: |- query { - Parrot2 as Parrot2(func: uid(0x123)) @filter(type(Parrot)) { + Parrot1(func: uid(0x123)) @filter(type(Parrot)) { uid } } @@ -2783,9 +3291,8 @@ "uid": "_:Human5" }], "dgraph.type": ["Home"], - "uid": "_:Home1" + "uid": "_:Home2" } - cond: "@if(eq(len(Parrot2), 1))" - name: "Add mutation with union - invalid input" diff --git a/graphql/resolve/auth_add_test.yaml b/graphql/resolve/auth_add_test.yaml index c952b33b8c9..e7aa83b7377 100644 --- a/graphql/resolve/auth_add_test.yaml +++ b/graphql/resolve/auth_add_test.yaml @@ -26,7 +26,9 @@ UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } authjson: | - { "UserSecret": [ { "uid": "0x123" }] } + { + "UserSecret": [ { "uid": "0x123" }] + } - name: "Add multiple nodes" gqlquery: | @@ -47,7 +49,10 @@ ] } uids: | - { "UserSecret1": "0x123", "UserSecret2": "0x456" } + { + "UserSecret1": "0x123", + "UserSecret2": "0x456" + } authquery: |- query { UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) { @@ -57,7 +62,9 @@ UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } authjson: | - { "UserSecret": [ { "uid": "0x123" }, { "uid": "0x456" } ] } + { + "UserSecret": [ { "uid": "0x123" }, { "uid": "0x456" } ] + } - name: "Add one node that fails auth" gqlquery: | @@ -71,13 +78,17 @@ jwtvar: USER: "user1" variables: | - { "secret": - { "aSecret": "it is", - "ownedBy": "user2" - } + { + "secret": + { + "aSecret": "it is", + "ownedBy": "user2" + } } uids: | - { "UserSecret1": "0x123" } + { + "UserSecret1": "0x123" + } authquery: |- query { UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) { @@ -87,7 +98,9 @@ UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } authjson: | - { "UserSecret": [ ] } + { + "UserSecret": [ ] + } error: { "message": "mutation failed because authorization failed" } @@ -110,7 +123,10 @@ ] } uids: | - { "UserSecret1": "0x123", "UserSecret2": "0x456" } + { + "UserSecret1": "0x123", + "UserSecret2": "0x456" + } authquery: |- query { UserSecret(func: uid(UserSecret1)) @filter(uid(UserSecretAuth2)) { @@ -120,7 +136,9 @@ UserSecretAuth2 as var(func: uid(UserSecret1)) @filter(eq(UserSecret.ownedBy, "user1")) @cascade } authjson: | - { "UserSecret": [ { "uid": "0x123" }] } + { + "UserSecret": [ { "uid": "0x123" }] + } error: { "message": "mutation failed because authorization failed" } @@ -144,14 +162,19 @@ } dgquery: |- query { - Project2 as Project2(func: uid(0x123)) @filter(type(Project)) { + Project1(func: uid(0x123)) @filter(type(Project)) { uid } } - json: | - { "Project2": [ { "uid": "0x123" } ] } + queryjson: | + { + "Project1": [ { "uid": "0x123" } ] + } uids: | - { "Column1": "0x456", "Ticket3": "0x789" } + { + "Column2": "0x456", + "Ticket3": "0x789" + } authquery: |- query { Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { @@ -206,14 +229,19 @@ } dgquery: |- query { - Project2 as Project2(func: uid(0x123)) @filter(type(Project)) { + Project1(func: uid(0x123)) @filter(type(Project)) { uid } } - json: | - { "Project2": [ { "uid": "0x123" } ] } + queryjson: | + { + "Project1": [ { "uid": "0x123" } ] + } uids: | - { "Column1": "0x456", "Ticket3": "0x789" } + { + "Column2": "0x456", + "Ticket3": "0x789" + } authquery: |- query { Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { @@ -242,7 +270,9 @@ } } authjson: | - { "Ticket": [ { "uid": "0x789" } ]} + { + "Ticket": [ { "uid": "0x789" } ] + } error: { "message": "mutation failed because authorization failed" } @@ -271,20 +301,21 @@ } dgquery: |- query { - Project2 as Project2(func: uid(0x123)) @filter(type(Project)) { - uid - } - Project5 as Project5(func: uid(0x123)) @filter(type(Project)) { + Project1(func: uid(0x123)) @filter(type(Project)) { uid } } - json: | + queryjson: | { - "Project2": [ { "uid": "0x123" } ], - "Project5": [ { "uid": "0x123" } ] + "Project1": [ { "uid": "0x123" } ] } uids: | - { "Column1": "0x456", "Ticket3": "0x789", "Column4": "0x459", "Ticket6": "0x799" } + { + "Column2": "0x456", + "Ticket3": "0x789", + "Column4": "0x459", + "Ticket5": "0x799" + } authquery: |- query { Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { @@ -343,20 +374,21 @@ } dgquery: |- query { - Project2 as Project2(func: uid(0x123)) @filter(type(Project)) { - uid - } - Project5 as Project5(func: uid(0x123)) @filter(type(Project)) { + Project1(func: uid(0x123)) @filter(type(Project)) { uid } } - json: | + queryjson: | { - "Project2": [ { "uid": "0x123" } ], - "Project5": [ { "uid": "0x123" } ] + "Project1": [ { "uid": "0x123" } ] } uids: | - { "Column1": "0x456", "Ticket3": "0x789", "Column4": "0x459", "Ticket6": "0x799" } + { + "Column2": "0x456", + "Ticket3": "0x789", + "Column4": "0x459", + "Ticket5": "0x799" + } authquery: |- query { Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { @@ -417,13 +449,21 @@ } dgquery: |- query { - Project2 as Project2(func: uid(0x123)) @filter(type(Project)) { + Project1(func: uid(0x123)) @filter(type(Project)) { uid } - Ticket3 as Ticket3(func: uid(0x789)) @filter(type(Ticket)) { + Ticket2(func: uid(0x789)) @filter(type(Ticket)) { uid } - var(func: uid(Ticket3)) { + } + queryjson: | + { + "Project1": [ { "uid": "0x123" } ], + "Ticket2": [ { "uid": "0x789" } ] + } + dgquerysec: |- + query { + var(func: uid(0x789)) { Column4 as Ticket.onColumn } Column4(func: uid(Column4)) { @@ -440,15 +480,15 @@ } } } + uids: | + { + "Column3": "0x456" + } json: | { - "Project2": [ { "uid": "0x123" } ], - "Ticket3": [ { "uid": "0x789" } ], "Column4": [ { "uid": "0x799" } ], "Column4.auth": [ { "uid": "0x799" } ] } - uids: | - { "Column1": "0x456" } authquery: |- query { Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { @@ -488,13 +528,21 @@ } dgquery: |- query { - Project2 as Project2(func: uid(0x123)) @filter(type(Project)) { + Project1(func: uid(0x123)) @filter(type(Project)) { uid } - Ticket3 as Ticket3(func: uid(0x789)) @filter(type(Ticket)) { + Ticket2(func: uid(0x789)) @filter(type(Ticket)) { uid } - var(func: uid(Ticket3)) { + } + queryjson: | + { + "Project1": [ { "uid": "0x123" } ], + "Ticket2": [ { "uid": "0x789" } ] + } + dgquerysec: |- + query { + var(func: uid(0x789)) { Column4 as Ticket.onColumn } Column4(func: uid(Column4)) { @@ -513,12 +561,12 @@ } json: | { - "Project2": [ { "uid": "0x123" } ], - "Ticket3": [ { "uid": "0x789" } ], "Column4": [ { "uid": "0x799" } ] } uids: | - { "Column1": "0x456" } + { + "Column3": "0x456" + } authquery: |- query { Column(func: uid(Column1)) @filter(uid(ColumnAuth2)) { @@ -564,10 +612,17 @@ } dgquery: |- query { - Ticket3 as Ticket3(func: uid(0x789)) @filter(type(Ticket)) { + Ticket1(func: uid(0x789)) @filter(type(Ticket)) { uid } - var(func: uid(Ticket3)) { + } + queryjson: | + { + "Ticket1": [ { "uid": "0x789" } ] + } + dgquerysec: |- + query { + var(func: uid(0x789)) { Column4 as Ticket.onColumn } Column4(func: uid(Column4)) { @@ -586,14 +641,13 @@ } json: | { - "Ticket3": [ { "uid": "0x789" } ], "Column4": [ { "uid": "0x799" } ], "Column4.auth": [ { "uid": "0x799" } ] } uids: | { - "Project1": "0x123", - "Column2": "0x456" + "Project2": "0x123", + "Column3": "0x456" } authquery: |- query { @@ -648,10 +702,17 @@ } dgquery: |- query { - Ticket3 as Ticket3(func: uid(0x789)) @filter(type(Ticket)) { + Ticket1(func: uid(0x789)) @filter(type(Ticket)) { uid } - var(func: uid(Ticket3)) { + } + queryjson: | + { + "Ticket1": [ { "uid": "0x789" } ] + } + dgquerysec: |- + query { + var(func: uid(0x789)) { Column4 as Ticket.onColumn } Column4(func: uid(Column4)) { @@ -670,13 +731,12 @@ } json: | { - "Ticket3": [ { "uid": "0x789" } ], "Column4": [ { "uid": "0x799" } ] } uids: | { - "Project1": "0x123", - "Column2": "0x456" + "Project2": "0x123", + "Column3": "0x456" } authquery: |- query { @@ -755,7 +815,9 @@ } } uids: | - { "Log1": "0x123" } + { + "Log1": "0x123" + } skipauth: true - name: "Add with top level OR RBAC true." @@ -846,21 +908,18 @@ } dgquery: |- query { - User3 as User3(func: eq(User.username, "user1")) @filter(type(User)) { + User1(func: eq(User.username, "user1")) @filter(type(User)) { uid } } - dgquerysec: |- - query { - User3 as User3(func: eq(User.username, "user1")) @filter(type(User)) { - uid - } + queryjson: | + { + "User1": [ { "uid": "0x123" } ] } - length: "2" - json: | - { "User3": [ { "uid": "0x123" } ] } uids: | - { "Issue1": "0x789" } + { + "Issue2": "0x789" + } authquery: |- query { Issue(func: uid(Issue1)) @filter(uid(IssueAuth2)) { @@ -872,7 +931,9 @@ } } authjson: | - { "Issue": [ { "uid": "0x789" }] } + { + "Issue": [ { "uid": "0x789" }] + } - name: "Add with top level And RBAC false." gqlquery: | @@ -897,21 +958,18 @@ } dgquery: |- query { - User3 as User3(func: eq(User.username, "user1")) @filter(type(User)) { + User1(func: eq(User.username, "user1")) @filter(type(User)) { uid } } - dgquerysec: |- - query { - User3 as User3(func: eq(User.username, "user1")) @filter(type(User)) { - uid - } + queryjson: | + { + "User1": [ { "uid": "0x123" } ] } - length: "2" - json: | - { "User3": [ { "uid": "0x123" } ] } uids: | - { "Issue1": "0x789" } + { + "Issue2": "0x789" + } authquery: |- query { Issue(func: uid(Issue1)) @filter(uid(Issue2)) { @@ -944,7 +1002,9 @@ } } uids: | - { "ComplexLog1": "0x123" } + { + "ComplexLog1": "0x123" + } error: { "message": "mutation failed because authorization failed"} @@ -967,7 +1027,9 @@ } } uids: | - { "ComplexLog1": "0x123" } + { + "ComplexLog1": "0x123" + } skipauth: true - name: "Adding nodes for a Type that inherits Auth rules from an interfaces successfully." @@ -998,7 +1060,10 @@ }] } uids: | - {"Question1": "0x123", "Author1": "0x456" } + { + "Question1": "0x123", + "Author1": "0x456" + } authquery: |- query { Question(func: uid(Question1)) @filter((uid(QuestionAuth2) AND uid(QuestionAuth3))) { @@ -1016,7 +1081,10 @@ } } authjson: | - {"Question": [ {"uid": "0x123"}]} + { + "Question": [ {"uid": "0x123"}] + } + - name: "Adding node for a Type that inherits auth rules from an interface fails." gqlquery: | mutation addQuestion($question: [AddQuestionInput!]!) { @@ -1045,7 +1113,10 @@ }] } uids: | - {"Question1": "0x123", "Author1": "0x456" } + { + "Question1": "0x123", + "Author1": "0x456" + } authquery: |- query { Question(func: uid(Question1)) @filter((uid(QuestionAuth2) AND uid(QuestionAuth3))) { @@ -1063,7 +1134,9 @@ } } authjson: | - {"Question": [ ], "Author": [ { "uid" : "0x456"} ] } + { + "Question": [ ], "Author": [ { "uid" : "0x456"} ] + } error: { "message": "mutation failed because authorization failed"} @@ -1093,7 +1166,10 @@ }] } uids: | - {"FbPost1": "0x123", "Author1": "0x456" } + { + "FbPost1": "0x123", + "Author1": "0x456" + } authquery: |- query { FbPost(func: uid(FbPost1)) @filter(uid(FbPostAuth2)) { @@ -1108,7 +1184,9 @@ } } authjson: | - {"FbPost": [ {"uid": "0x123"}]} + { + "FbPost": [ {"uid": "0x123"}] + } - name: "Add type with Having RBAC rule on interface failed" gqlquery: | @@ -1136,7 +1214,10 @@ }] } uids: | - {"FbPost1": "0x123", "Author1": "0x456" } + { + "FbPost1": "0x123", + "Author1": "0x456" + } error: {"message" : "mutation failed because authorization failed"} \ No newline at end of file diff --git a/graphql/resolve/auth_delete_test.yaml b/graphql/resolve/auth_delete_test.yaml index 7d3ddee09f8..21e9587b7c5 100644 --- a/graphql/resolve/auth_delete_test.yaml +++ b/graphql/resolve/auth_delete_test.yaml @@ -56,12 +56,12 @@ } TweetsRoot as var(func: uid(Tweets1)) Tweets1 as var(func: type(Tweets)) @filter(anyoftext(Tweets.text, "abc")) - tweets(func: uid(Tweets3)) { + tweets(func: uid(Tweets4)) { text : Tweets.text dgraph.uid : uid } - Tweets3 as var(func: uid(Tweets4)) - Tweets4 as var(func: uid(x)) + Tweets4 as var(func: uid(Tweets5)) + Tweets5 as var(func: uid(x)) } - name: "Delete with inverse field and RBAC false" @@ -86,12 +86,12 @@ dgquery: |- query { x as deleteTweets() - tweets(func: uid(Tweets1)) { + tweets(func: uid(Tweets2)) { text : Tweets.text dgraph.uid : uid } - Tweets1 as var(func: uid(Tweets2)) - Tweets2 as var(func: uid(x)) + Tweets2 as var(func: uid(Tweets3)) + Tweets3 as var(func: uid(x)) } - name: "Delete with deep auth" @@ -195,12 +195,12 @@ } } } - ticket(func: uid(Ticket5)) { + ticket(func: uid(Ticket6)) { title : Ticket.title - onColumn : Ticket.onColumn @filter(uid(Column6)) { - inProject : Column.inProject @filter(uid(Project8)) { - roles : Project.roles @filter(uid(Role10)) { - assignedTo : Role.assignedTo @filter(uid(User12)) { + onColumn : Ticket.onColumn @filter(uid(Column7)) { + inProject : Column.inProject @filter(uid(Project9)) { + roles : Project.roles @filter(uid(Role11)) { + assignedTo : Role.assignedTo @filter(uid(User13)) { username : User.username age : User.age dgraph.uid : uid @@ -213,9 +213,9 @@ } dgraph.uid : uid } - Ticket5 as var(func: uid(Ticket16)) @filter(uid(TicketAuth17)) - Ticket16 as var(func: uid(x)) - TicketAuth17 as var(func: uid(Ticket16)) @cascade { + Ticket6 as var(func: uid(Ticket17)) @filter(uid(TicketAuth18)) + Ticket17 as var(func: uid(x)) + TicketAuth18 as var(func: uid(Ticket17)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { @@ -224,28 +224,28 @@ } } } - var(func: uid(Ticket5)) { - Column7 as Ticket.onColumn + var(func: uid(Ticket6)) { + Column8 as Ticket.onColumn } - Column6 as var(func: uid(Column7)) @filter(uid(ColumnAuth15)) - var(func: uid(Column6)) { - Project9 as Column.inProject + Column7 as var(func: uid(Column8)) @filter(uid(ColumnAuth16)) + var(func: uid(Column7)) { + Project10 as Column.inProject } - Project8 as var(func: uid(Project9)) @filter(uid(ProjectAuth14)) - var(func: uid(Project8)) { - Role11 as Project.roles + Project9 as var(func: uid(Project10)) @filter(uid(ProjectAuth15)) + var(func: uid(Project9)) { + Role12 as Project.roles } - Role10 as var(func: uid(Role11)) - var(func: uid(Role10)) { - User13 as Role.assignedTo + Role11 as var(func: uid(Role12)) + var(func: uid(Role11)) { + User14 as Role.assignedTo } - User12 as var(func: uid(User13)) - ProjectAuth14 as var(func: uid(Project9)) @cascade { + User13 as var(func: uid(User14)) + ProjectAuth15 as var(func: uid(Project10)) @cascade { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) } } - ColumnAuth15 as var(func: uid(Column7)) @cascade { + ColumnAuth16 as var(func: uid(Column8)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "VIEW")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) @@ -448,13 +448,13 @@ } LogRoot as var(func: uid(Log1)) Log1 as var(func: uid(0x1, 0x2)) @filter(type(Log)) - log(func: uid(Log2), orderasc: Log.logs) { + log(func: uid(Log3), orderasc: Log.logs) { logs : Log.logs random : Log.random dgraph.uid : uid } - Log2 as var(func: uid(Log3), orderasc: Log.logs) - Log3 as var(func: uid(x)) + Log3 as var(func: uid(Log4), orderasc: Log.logs) + Log4 as var(func: uid(x)) } - name: "Delete with top level OR RBAC true." diff --git a/graphql/resolve/auth_test.go b/graphql/resolve/auth_test.go index 3b56253b3cc..6d1ac2e9a29 100644 --- a/graphql/resolve/auth_test.go +++ b/graphql/resolve/auth_test.go @@ -21,7 +21,6 @@ import ( "encoding/json" "fmt" "io/ioutil" - "strconv" "strings" "testing" @@ -61,8 +60,10 @@ type AuthQueryRewritingCase struct { Length string // UIDS and json from the Dgraph result - Uids string - Json string + Uids string + Json string + QueryJSON string + DeleteQuery string // Post-mutation auth query and result Dgraph returns from that query AuthQuery string @@ -76,14 +77,18 @@ type AuthQueryRewritingCase struct { } type authExecutor struct { - t *testing.T - state int - length int + t *testing.T + state int + + // existence query and its result in JSON + dgQuery string + queryResultJSON string // initial mutation - upsertQuery []string - json string - uids string + dgQuerySec string + // json is the response of the query following the mutation + json string + uids string // auth authQuery string @@ -94,41 +99,48 @@ type authExecutor struct { func (ex *authExecutor) Execute(ctx context.Context, req *dgoapi.Request) (*dgoapi.Response, error) { ex.state++ + // Existence Query is not executed if it is empty. Increment the state value. + if ex.dgQuery == "" && ex.state == 1 { + ex.state++ + } switch ex.state { case 1: - // initial mutation - ex.length -= 1 + // existence query. + require.Equal(ex.t, ex.dgQuery, req.Query) - // check that the upsert has built in auth, if required - require.Equal(ex.t, ex.upsertQuery[ex.length], req.Query) + // Return mocked result of existence query. + return &dgoapi.Response{ + Json: []byte(ex.queryResultJSON), + }, nil + case 2: + // mutation to create new nodes var assigned map[string]string if ex.uids != "" { err := json.Unmarshal([]byte(ex.uids), &assigned) require.NoError(ex.t, err) } + // Check query generated along with mutation. + require.Equal(ex.t, ex.dgQuerySec, req.Query) + if len(assigned) == 0 { - // skip state 2, there's no new nodes to apply auth to + // skip state 3, there's no new nodes to apply auth to ex.state++ } - // For rules that don't require auth, it should directly go to step 3. + // For rules that don't require auth, it should directly go to step 4. if ex.skipAuth { ex.state++ } - if ex.length != 0 { - ex.state = 0 - } - return &dgoapi.Response{ Json: []byte(ex.json), Uids: assigned, Metrics: &dgoapi.Metrics{NumUids: map[string]uint64{touchedUidsKey: 0}}, }, nil - case 2: + case 3: // auth // check that we got the expected auth query @@ -140,7 +152,7 @@ func (ex *authExecutor) Execute(ctx context.Context, req *dgoapi.Request) (*dgoa Metrics: &dgoapi.Metrics{NumUids: map[string]uint64{touchedUidsKey: 0}}, }, nil - case 3: + case 4: // final result return &dgoapi.Response{ @@ -410,11 +422,12 @@ func queryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMeta, b []b // Tests that the queries that run after a mutation get auth correctly added in. func mutationQueryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMeta) { tests := map[string]struct { - gqlMut string - rewriter func() MutationRewriter - assigned map[string]string - result map[string]interface{} - dgQuery string + gqlMut string + rewriter func() MutationRewriter + assigned map[string]string + idExistence map[string]string + result map[string]interface{} + dgQuery string }{ "Add Ticket": { gqlMut: `mutation { @@ -429,8 +442,9 @@ func mutationQueryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMet } } }`, - rewriter: NewAddRewriter, - assigned: map[string]string{"Ticket1": "0x4"}, + rewriter: NewAddRewriter, + assigned: map[string]string{"Ticket2": "0x4"}, + idExistence: map[string]string{"Column1": "0x1"}, dgQuery: `query { ticket(func: uid(TicketRoot)) { id : uid @@ -477,7 +491,8 @@ func mutationQueryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMet } } }`, - rewriter: NewUpdateRewriter, + rewriter: NewUpdateRewriter, + idExistence: map[string]string{}, result: map[string]interface{}{ "updateTicket": []interface{}{map[string]interface{}{"uid": "0x4"}}}, dgQuery: `query { @@ -531,7 +546,8 @@ func mutationQueryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMet ctx, err := authMeta.AddClaimsToContext(context.Background()) require.NoError(t, err) - _, err = rewriter.Rewrite(ctx, gqlMutation) + _, _ = rewriter.RewriteQueries(context.Background(), gqlMutation) + _, err = rewriter.Rewrite(ctx, gqlMutation, tt.idExistence) require.Nil(t, err) // -- Act -- @@ -605,7 +621,9 @@ func deleteQueryRewriting(t *testing.T, sch string, authMeta *testutil.AuthMeta, } // -- Act -- - upsert, err := rewriterToTest.Rewrite(ctx, mut) + _, _ = rewriterToTest.RewriteQueries(context.Background(), mut) + idExistence := make(map[string]string) + upsert, err := rewriterToTest.Rewrite(ctx, mut, idExistence) // -- Assert -- if tcase.Error != nil || err != nil { @@ -712,26 +730,16 @@ func checkAddUpdateCase( require.NoError(t, err) } - length := 1 - upsertQuery := []string{tcase.DGQuery} - - if tcase.Length != "" { - length, _ = strconv.Atoi(tcase.Length) - } - - if length == 2 { - upsertQuery = []string{tcase.DGQuerySec, tcase.DGQuery} - } - ex := &authExecutor{ - t: t, - upsertQuery: upsertQuery, - json: tcase.Json, - uids: tcase.Uids, - authQuery: tcase.AuthQuery, - authJson: tcase.AuthJson, - skipAuth: tcase.SkipAuth, - length: length, + t: t, + json: tcase.Json, + queryResultJSON: tcase.QueryJSON, + dgQuerySec: tcase.DGQuerySec, + uids: tcase.Uids, + dgQuery: tcase.DGQuery, + authQuery: tcase.AuthQuery, + authJson: tcase.AuthJson, + skipAuth: tcase.SkipAuth, } resolver := NewDgraphResolver(rewriter(), ex, StdMutationCompletion(mut.ResponseName())) @@ -776,12 +784,10 @@ func TestAuthQueryRewriting(t *testing.T) { t.Run("Mutation Query Rewriting "+algo, func(t *testing.T) { mutationQueryRewriting(t, strSchema, metaInfo) }) - b = read(t, "auth_add_test.yaml") t.Run("Add Mutation "+algo, func(t *testing.T) { mutationAdd(t, strSchema, metaInfo, b) }) - b = read(t, "auth_update_test.yaml") t.Run("Update Mutation "+algo, func(t *testing.T) { mutationUpdate(t, strSchema, metaInfo, b) diff --git a/graphql/resolve/auth_update_test.yaml b/graphql/resolve/auth_update_test.yaml index ef44dc320ae..2e01932e416 100644 --- a/graphql/resolve/auth_update_test.yaml +++ b/graphql/resolve/auth_update_test.yaml @@ -15,7 +15,7 @@ "set": { "aSecret": "new Value" } } } - dgquery: |- + dgquerysec: |- query { x as updateUserSecret(func: uid(UserSecretRoot)) { uid @@ -48,7 +48,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateColumn(func: uid(ColumnRoot)) { uid @@ -110,7 +110,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateColumn(func: uid(ColumnRoot)) { uid @@ -176,23 +176,30 @@ } } dgquery: |- + query { + Ticket1(func: uid(0x789)) @filter(type(Ticket)) { + uid + } + } + queryjson: | + { + "Ticket1": [ { "uid": "0x789" } ] + } + dgquerysec: |- query { x as updateColumn(func: uid(ColumnRoot)) { uid } - ColumnRoot as var(func: uid(Column1)) @filter(uid(ColumnAuth2)) - Column1 as var(func: uid(0x123)) @filter(type(Column)) - ColumnAuth2 as var(func: uid(Column1)) @cascade { + ColumnRoot as var(func: uid(Column2)) @filter(uid(ColumnAuth3)) + Column2 as var(func: uid(0x123)) @filter(type(Column)) + ColumnAuth3 as var(func: uid(Column2)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) } } } - Ticket4 as Ticket4(func: uid(0x789)) @filter(type(Ticket)) { - uid - } - var(func: uid(Ticket4)) { + var(func: uid(0x789)) { Column5 as Ticket.onColumn @filter(NOT (uid(x))) } Column5(func: uid(Column5)) { @@ -239,23 +246,30 @@ } } dgquery: |- + query { + Ticket1(func: uid(0x789)) @filter(type(Ticket)) { + uid + } + } + queryjson: | + { + "Ticket1": [ { "uid": "0x789" } ] + } + dgquerysec: |- query { x as updateColumn(func: uid(ColumnRoot)) { uid } - ColumnRoot as var(func: uid(Column1)) @filter(uid(ColumnAuth2)) - Column1 as var(func: uid(0x123)) @filter(type(Column)) - ColumnAuth2 as var(func: uid(Column1)) @cascade { + ColumnRoot as var(func: uid(Column2)) @filter(uid(ColumnAuth3)) + Column2 as var(func: uid(0x123)) @filter(type(Column)) + ColumnAuth3 as var(func: uid(Column2)) @cascade { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "ADMIN")) { assignedTo : Role.assignedTo @filter(eq(User.username, "user1")) } } } - Ticket4 as Ticket4(func: uid(0x789)) @filter(type(Ticket)) { - uid - } - var(func: uid(Ticket4)) { + var(func: uid(0x789)) { Column5 as Ticket.onColumn @filter(NOT (uid(x))) } Column5(func: uid(Column5)) { @@ -278,6 +292,11 @@ "Ticket4": [ { "uid": "0x789" } ], "Column5": [ { "uid": "0x456" } ] } + authquery: |- + query { + } + authjson: | + { } error: { "message": "couldn't rewrite query for mutation updateColumn because authorization failed" } @@ -303,13 +322,23 @@ } } dgquery: |- + query { + Column1(func: uid(0x456)) @filter(type(Column)) { + uid + } + } + queryjson: | + { + "Column1": [ { "uid": "0x456" } ] + } + dgquerysec: |- query { x as updateTicket(func: uid(TicketRoot)) { uid } - TicketRoot as var(func: uid(Ticket1)) @filter(uid(TicketAuth2)) - Ticket1 as var(func: uid(0x123)) @filter(type(Ticket)) - TicketAuth2 as var(func: uid(Ticket1)) @cascade { + TicketRoot as var(func: uid(Ticket2)) @filter(uid(TicketAuth3)) + Ticket2 as var(func: uid(0x123)) @filter(type(Ticket)) + TicketAuth3 as var(func: uid(Ticket2)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -318,11 +347,8 @@ } } } - Column4 as Column4(func: uid(0x456)) @filter(type(Column)) { - uid - } var(func: uid(x)) { - Column5 as Ticket.onColumn @filter(NOT (uid(Column4))) + Column5 as Ticket.onColumn @filter(NOT (uid(0x456))) } Column5(func: uid(Column5)) { uid @@ -367,13 +393,23 @@ } } dgquery: |- + query { + Column1(func: uid(0x456)) @filter(type(Column)) { + uid + } + } + queryjson: | + { + "Column1": [ { "uid": "0x456" } ] + } + dgquerysec: |- query { x as updateTicket(func: uid(TicketRoot)) { uid } - TicketRoot as var(func: uid(Ticket1)) @filter(uid(TicketAuth2)) - Ticket1 as var(func: uid(0x123)) @filter(type(Ticket)) - TicketAuth2 as var(func: uid(Ticket1)) @cascade { + TicketRoot as var(func: uid(Ticket2)) @filter(uid(TicketAuth3)) + Ticket2 as var(func: uid(0x123)) @filter(type(Ticket)) + TicketAuth3 as var(func: uid(Ticket2)) @cascade { onColumn : Ticket.onColumn { inProject : Column.inProject { roles : Project.roles @filter(eq(Role.permission, "EDIT")) { @@ -382,11 +418,8 @@ } } } - Column4 as Column4(func: uid(0x456)) @filter(type(Column)) { - uid - } var(func: uid(x)) { - Column5 as Ticket.onColumn @filter(NOT (uid(Column4))) + Column5 as Ticket.onColumn @filter(NOT (uid(0x456))) } Column5(func: uid(Column5)) { uid @@ -432,7 +465,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateLog() } @@ -459,7 +492,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateLog(func: uid(LogRoot)) { uid @@ -489,7 +522,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateProject(func: uid(ProjectRoot)) { uid @@ -524,7 +557,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateProject(func: uid(ProjectRoot)) { uid @@ -554,7 +587,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateIssue(func: uid(IssueRoot)) { uid @@ -587,7 +620,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateIssue() } @@ -613,7 +646,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateComplexLog(func: uid(ComplexLogRoot)) { uid @@ -643,7 +676,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateComplexLog() } @@ -670,7 +703,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateQuestion(func: uid(QuestionRoot)) { uid @@ -707,7 +740,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateQuestion() } @@ -734,7 +767,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateFbPost(func: uid(FbPostRoot)) { uid @@ -771,7 +804,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateFbPost() } @@ -799,7 +832,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updatePost(func: uid(PostRoot)) { uid @@ -854,7 +887,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updatePost(func: uid(PostRoot)) { uid @@ -899,7 +932,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updatePost() } @@ -926,7 +959,7 @@ } } } - dgquery: |- + dgquerysec: |- query { x as updateA(func: uid(ARoot)) { uid diff --git a/graphql/resolve/extensions_test.go b/graphql/resolve/extensions_test.go index c06881a5057..71d8959df03 100644 --- a/graphql/resolve/extensions_test.go +++ b/graphql/resolve/extensions_test.go @@ -133,8 +133,10 @@ func TestMutationsPropagateExtensions(t *testing.T) { resp := resolveWithClient(gqlSchema, mutation, nil, &executor{ - queryTouched: 2, - mutationTouched: 5, + assigned: map[string]string{"Post1": "0x2"}, + existenceQueriesResp: `{ "Author1": [{"uid":"0x1"}]}`, + queryTouched: 2, + mutationTouched: 5, }) require.NotNil(t, resp) @@ -187,8 +189,10 @@ func TestMultipleMutationsPropagateExtensionsCorrectly(t *testing.T) { resp := resolveWithClient(gqlSchema, mutation, nil, &executor{ - queryTouched: 2, - mutationTouched: 5, + assigned: map[string]string{"Post1": "0x2"}, + existenceQueriesResp: `{ "Author1": [{"uid":"0x1"}]}`, + queryTouched: 2, + mutationTouched: 5, }) require.NotNil(t, resp) diff --git a/graphql/resolve/mutation.go b/graphql/resolve/mutation.go index 5b9c72d07ef..2e0ef799550 100644 --- a/graphql/resolve/mutation.go +++ b/graphql/resolve/mutation.go @@ -19,6 +19,7 @@ package resolve import ( "context" "encoding/json" + "fmt" "sort" "strconv" @@ -29,6 +30,7 @@ import ( "github.com/dgraph-io/dgraph/graphql/schema" "github.com/dgraph-io/dgraph/x" "github.com/golang/glog" + "github.com/pkg/errors" otrace "go.opencensus.io/trace" ) @@ -85,11 +87,22 @@ type MutationResolver interface { // mutation query rewriting is dependent on the context set up by the result of // the mutation. type MutationRewriter interface { + // RewriteQueries generates and rewrites GraphQL mutation m into DQL queries which + // check if any referenced node by XID or ID exist or not. + // Example existence queries: + // 1. Parrot1(func: uid(0x127)) @filter(type: Parrot) { + // uid + // } + // 2. Computer2(func: eq(Computer.name, "computer1")) @filter(type(Computer)) { + // uid + // } + // These query will be created in case of Add or Update Mutation which references node + // 0x127 or Computer of name "computer1" + RewriteQueries(ctx context.Context, m schema.Mutation) ([]*gql.GraphQuery, error) // Rewrite rewrites GraphQL mutation m into a Dgraph mutation - that could // be as simple as a single DelNquads, or could be a Dgraph upsert mutation // with a query and multiple mutations guarded by conditions. - Rewrite(ctx context.Context, m schema.Mutation) ([]*UpsertMutation, error) - + Rewrite(ctx context.Context, m schema.Mutation, idExistence map[string]string) ([]*UpsertMutation, error) // FromMutationResult takes a GraphQL mutation and the results of a Dgraph // mutation and constructs a Dgraph query. It's used to find the return // value from a GraphQL mutation - i.e. we've run the mutation indicated by m @@ -199,9 +212,11 @@ func getNumUids(m schema.Mutation, a map[string]string, r map[string]interface{} } } -func (mr *dgraphResolver) rewriteAndExecute(ctx context.Context, +func (mr *dgraphResolver) rewriteAndExecute( + ctx context.Context, mutation schema.Mutation) (*Resolved, bool) { var mutResp *dgoapi.Response + req := &dgoapi.Request{} commit := false defer func() { @@ -240,7 +255,87 @@ func (mr *dgraphResolver) rewriteAndExecute(ctx context.Context, } } - upserts, err := mr.mutationRewriter.Rewrite(ctx, mutation) + // upserts stores rewritten []*UpsertMutation by Rewrite function. These mutations + // are then executed and the results processed and returned. + var upserts []*UpsertMutation + var err error + // queries stores rewritten []*gql.GraphQuery by RewriteQueries function. These queries + // are then executed and the results are processed + var queries []*gql.GraphQuery + queries, err = mr.mutationRewriter.RewriteQueries(ctx, mutation) + if err != nil { + return emptyResult(schema.GQLWrapf(err, "couldn't rewrite mutation %s", mutation.Name())), + resolverFailed + } + // Execute queries and parse its result into a map + qry := dgraph.AsString(queries) + req.Query = qry + + // The query will be empty in case there is no reference XID / UID in the mutation. + // Don't execute the query in those cases. + // The query will also be empty in case this is not an Add or an Update Mutation. + if req.Query != "" { + mutResp, err = mr.executor.Execute(ctx, req) + } + if err != nil { + gqlErr := schema.GQLWrapLocationf( + err, mutation.Location(), "mutation %s failed", mutation.Name()) + return emptyResult(gqlErr), resolverFailed + } + + // Parse the result of query. + // mutResp.Json will contain response to the query. + // The response is parsed to existenceQueriesResult + // Example Response: + // { + // Project1 : + // [ + // { + // "uid" : "0x123" + // } + // ], + // Column2 : + // [ + // { + // "uid": "0x234" + // } + // ] + // } + queryResultMap := make(map[string][]map[string]string) + if mutResp != nil { + err = json.Unmarshal(mutResp.Json, &queryResultMap) + } + if err != nil { + gqlErr := schema.GQLWrapLocationf( + err, mutation.Location(), "mutation %s failed", mutation.Name()) + return emptyResult(gqlErr), resolverFailed + } + + // The above response is parsed into map[string]string as follows: + // { + // "Project1" : "0x123", + // "Column2" : "0x234" + // } + // As only Add and Update mutations generate queries using RewriteQueries, + // qNameToUID map will be non-empty only in case of Add or Update Mutation. + qNameToUID := make(map[string]string) + for key, result := range queryResultMap { + if len(result) == 1 { + // Found exactly one UID / XID corresponding to given condition + qNameToUID[key] = result[0]["uid"] + } else if len(result) > 1 { + // Found multiple UIDs for query. This should ideally not happen. + // This indicates that there are multiple nodes with same XIDs / UIDs. Throw an error. + err = errors.New(fmt.Sprintf("Found multiple nodes with ID: %s", result[0]["uid"])) + gqlErr := schema.GQLWrapLocationf( + err, mutation.Location(), "mutation %s failed", mutation.Name()) + return emptyResult(gqlErr), resolverFailed + } + } + + // Create upserts, delete mutations, update mutations, add mutations. + upserts, err = mr.mutationRewriter.Rewrite(ctx, mutation, qNameToUID) + if err != nil { return emptyResult(schema.GQLWrapf(err, "couldn't rewrite mutation %s", mutation.Name())), resolverFailed @@ -259,7 +354,6 @@ func (mr *dgraphResolver) rewriteAndExecute(ctx context.Context, } result := make(map[string]interface{}) - req := &dgoapi.Request{} newNodes := make(map[string]schema.Type) mutationTimer := newtimer(ctx, &dgraphMutationDuration.OffsetDuration) @@ -295,7 +389,8 @@ func (mr *dgraphResolver) rewriteAndExecute(ctx context.Context, } var errs error - dgQuery, err := mr.mutationRewriter.FromMutationResult(ctx, mutation, mutResp.GetUids(), result) + var dgQuery []*gql.GraphQuery + dgQuery, err = mr.mutationRewriter.FromMutationResult(ctx, mutation, mutResp.GetUids(), result) errs = schema.AppendGQLErrs(errs, schema.GQLWrapf(err, "couldn't rewrite query for mutation %s", mutation.Name())) if dgQuery == nil && err != nil { diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index a73ea6c0c6e..bf9c7bd23bd 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -40,14 +40,43 @@ const ( updateMutationCondition = `gt(len(x), 0)` ) +// Enum passed on to rewriteObject function. +type MutationType int + +const ( + // Add Mutation + Add MutationType = iota + // Update Mutation used for to setting new nodes, edges. + UpdateWithSet + // Update Mutation used for removing edges. + UpdateWithRemove +) + +type Rewriter struct { + // VarGen is the VariableGenerator used accross RewriteQueries and Rewrite functions + // for Mutation. It generates unique variable names for DQL queries and mutations. + VarGen *VariableGenerator + // XidMetadata stores data like seenUIDs and variableObjMap to be used across Rewrite + // and RewriteQueries functions for Mutations. + XidMetadata *xidMetadata + // idExistence stores a map of variable names to UIDs. It is a map of nodes which + // were found after executing queries generated by RewriteQueries function. This is + // used in case of Add and Update Mutations. + idExistence map[string]string +} + type AddRewriter struct { frags [][]*mutationFragment + Rewriter } type UpdateRewriter struct { setFrags []*mutationFragment delFrags []*mutationFragment + Rewriter +} +type deleteRewriter struct { + Rewriter } -type deleteRewriter struct{} // A mutationFragment is a partially built Dgraph mutation. Given a GraphQL // mutation input, we traverse the input data and build a Dgraph mutation. That @@ -76,8 +105,8 @@ type xidMetadata struct { variableObjMap map[string]map[string]interface{} // seenAtTopLevel tells whether the xidVariable has been previously seen at top level or not seenAtTopLevel map[string]bool - // queryExists tells whether the query part in upsert has already been created for xidVariable - queryExists map[string]bool + // seenUIDs tells whether the UID is previously been seen during DFS traversal + seenUIDs map[string]bool } // A mutationBuilder can build a json mutation []byte from a mutationFragment @@ -148,12 +177,12 @@ func NewDeleteRewriter() MutationRewriter { return &deleteRewriter{} } -// newXidMetadata returns a new empty *xidMetadata for storing the metadata. -func newXidMetadata() *xidMetadata { +// NewXidMetadata returns a new empty *xidMetadata for storing the metadata. +func NewXidMetadata() *xidMetadata { return &xidMetadata{ variableObjMap: make(map[string]map[string]interface{}), seenAtTopLevel: make(map[string]bool), - queryExists: make(map[string]bool), + seenUIDs: make(map[string]bool), } } @@ -184,8 +213,137 @@ func (xidMetadata *xidMetadata) isDuplicateXid(atTopLevel bool, xidVar string, return false } +// RewriteQueries takes a GraphQL schema.Mutation add and creates queries to find out if +// referenced nodes by XID and UID exist or not. +// m must have a single argument called 'input' that carries the mutation data. +// +// For example, a GraphQL add mutation to add an object of type Author, +// with GraphQL input object (where country code is @id) +// +// { +// name: "A.N. Author", +// country: { code: "ind", name: "India" }, +// posts: [ { title: "A Post", text: "Some text" }] +// friends: [ { id: "0x123" } ] +// } +// +// The following queries would be generated +// query { +// Country2(func: eq(Country.code, "ind")) @filter(type: Country) { +// uid +// } +// Person3(func: uid(0x123)) @filter(type: Person) { +// uid +// } +// } +// +// This query will be executed and depending on the result it would be decided whether +// to create a new country as part of this mutation or link it to an existing country. +// If it is found out that there is an existing country, no modifications are made to +// the country's attributes and its children. Mutations of the country's children are +// simply ignored. +// If it is found out that the Person with id 0x123 does not exist, the corresponding +// mutation will fail. +func (mrw *AddRewriter) RewriteQueries( + ctx context.Context, + m schema.Mutation) ([]*gql.GraphQuery, error) { + + mrw.VarGen = NewVariableGenerator() + mrw.XidMetadata = NewXidMetadata() + + mutatedType := m.MutatedType() + val, _ := m.ArgValue(schema.InputArgName).([]interface{}) + + var ret []*gql.GraphQuery + var retErrors error + + for _, i := range val { + obj := i.(map[string]interface{}) + queries, errs := existenceQueries(ctx, mutatedType, nil, mrw.VarGen, obj, mrw.XidMetadata) + if len(errs) > 0 { + var gqlErrors x.GqlErrorList + for _, err := range errs { + gqlErrors = append(gqlErrors, schema.AsGQLErrors(err)...) + } + retErrors = schema.AppendGQLErrs(retErrors, schema.GQLWrapf(gqlErrors, + "failed to rewrite mutation payload")) + } + ret = append(ret, queries...) + } + return ret, retErrors +} + +// RewriteQueries creates and rewrites set and remove update patches queries. +// The GraphQL updates look like: +// +// input UpdateAuthorInput { +// filter: AuthorFilter! +// set: PatchAuthor +// remove: PatchAuthor +// } +// +// which gets rewritten in to a DQL queries to check if +// - referenced UIDs and XIDs in set and remove exist or not. +// +// Depending on the result of these executed queries, it is then decided whether to +// create new nodes or link to existing ones. +// +// Note that queries rewritten using RewriteQueries don't include UIDs or XIDs referenced +// as part of filter argument. +// +// See AddRewriter for how the rewritten queries look like. +func (urw *UpdateRewriter) RewriteQueries( + ctx context.Context, + m schema.Mutation) ([]*gql.GraphQuery, error) { + mutatedType := m.MutatedType() + + urw.VarGen = NewVariableGenerator() + urw.XidMetadata = NewXidMetadata() + + inp := m.ArgValue(schema.InputArgName).(map[string]interface{}) + setArg := inp["set"] + delArg := inp["remove"] + + var ret []*gql.GraphQuery + var retErrors error + + // Write existence queries for set + if setArg != nil { + obj := setArg.(map[string]interface{}) + queries, errs := existenceQueries(ctx, mutatedType, nil, urw.VarGen, obj, urw.XidMetadata) + if len(errs) > 0 { + var gqlErrors x.GqlErrorList + for _, err := range errs { + gqlErrors = append(gqlErrors, schema.AsGQLErrors(err)...) + } + retErrors = schema.AppendGQLErrs(retErrors, schema.GQLWrapf(gqlErrors, + "failed to rewrite mutation payload")) + } + ret = append(ret, queries...) + } + + // Write existence queries for remove + if delArg != nil { + obj := delArg.(map[string]interface{}) + queries, errs := existenceQueries(ctx, mutatedType, nil, urw.VarGen, obj, urw.XidMetadata) + if len(errs) > 0 { + var gqlErrors x.GqlErrorList + for _, err := range errs { + gqlErrors = append(gqlErrors, schema.AsGQLErrors(err)...) + } + retErrors = schema.AppendGQLErrs(retErrors, schema.GQLWrapf(gqlErrors, + "failed to rewrite mutation payload")) + } + ret = append(ret, queries...) + } + return ret, retErrors +} + // Rewrite takes a GraphQL schema.Mutation add and builds a Dgraph upsert mutation. // m must have a single argument called 'input' that carries the mutation data. +// The arguments also consist of idExistence map which is a map from +// Variable Name --> UID . This map is used to know which referenced nodes exists and +// whether to link the newly created node to existing node or create a new one. // // That argument could have been passed in the mutation like: // @@ -208,50 +366,20 @@ func (xidMetadata *xidMetadata) isDuplicateXid(atTopLevel bool, xidVar string, // posts: [ { title: "A Post", text: "Some text" }] // friends: [ { id: "0x123" } ] // } -// -// becomes a guarded upsert with two possible paths - one if "ind" already exists -// and the other if we create "ind" as part of the mutation. -// -// Query: -// query { -// Author4 as Author4(func: uid(0x123)) @filter(type(Author)) { -// uid -// } -// Country2 as Country2(func: eq(Country.code, "ind")) @filter(type(Country)) { -// uid -// } -// } -// -// And two conditional mutations. Both create a new post and check that the linked -// friend is an Author. One links to India if it exists, the other creates it -// -// "@if(eq(len(Country2), 0) AND eq(len(Author4), 1))" +// and idExistence // { -// "uid":"_:Author1" -// "dgraph.type":["Author"], -// "Author.name":"A.N. Author", -// "Author.country":{ -// "uid":"_:Country2", -// "dgraph.type":["Country"], -// "Country.code":"ind", -// "Country.name":"India" -// }, -// "Author.posts": [ { -// "uid":"_:Post3" -// "dgraph.type":["Post"], -// "Post.text":"Some text", -// "Post.title":"A Post", -// } ], -// "Author.friends":[ {"uid":"0x123"} ], +// "Country2": "0x234", +// "Person3": "0x123" // } // -// and @if(eq(len(Country2), 1) AND eq(len(Author4), 1)) +// becomes an unconditional mutation. +// // { // "uid":"_:Author1", // "dgraph.type":["Author"], // "Author.name":"A.N. Author", // "Author.country": { -// "uid":"uid(Country2)" +// "uid":"0x234" // }, // "Author.posts": [ { // "uid":"_:Post3" @@ -261,22 +389,45 @@ func (xidMetadata *xidMetadata) isDuplicateXid(atTopLevel bool, xidVar string, // } ], // "Author.friends":[ {"uid":"0x123"} ], // } -func (mrw *AddRewriter) Rewrite(ctx context.Context, m schema.Mutation) ([]*UpsertMutation, error) { +func (mrw *AddRewriter) Rewrite( + ctx context.Context, + m schema.Mutation, + idExistence map[string]string) ([]*UpsertMutation, error) { mutatedType := m.MutatedType() val, _ := m.ArgValue(schema.InputArgName).([]interface{}) - varGen := NewVariableGenerator() - xidMd := newXidMetadata() - var errs error - mutationsAllSec := []*dgoapi.Mutation{} - queriesSec := &gql.GraphQuery{} + varGen := mrw.VarGen + xidMetadata := mrw.XidMetadata + // ret stores a slice of Upsert Mutations. These are used in executing upsert queries in graphql/resolve/mutation.go + var ret []*UpsertMutation + // fragments stores a slice of mutationFragments. This is used in constructing mutationsAll which is returned back to the caller + // of this function as UpsertMutation.mutation + var fragments []*mutationFragment + var retErrors error + + for _, i := range val { + obj := i.(map[string]interface{}) + fragment, errs := rewriteObject(ctx, mutatedType, nil, "", varGen, obj, xidMetadata, idExistence, Add) + if len(errs) > 0 { + var gqlErrors x.GqlErrorList + for _, err := range errs { + gqlErrors = append(gqlErrors, schema.AsGQLErrors(err)...) + } + retErrors = schema.AppendGQLErrs(retErrors, schema.GQLWrapf(gqlErrors, + "failed to rewrite mutation payload")) + } + if fragment != nil { + fragments = append(fragments, fragment) + mrw.frags = append(mrw.frags, []*mutationFragment{fragment}) + } + } mutationsAll := []*dgoapi.Mutation{} queries := &gql.GraphQuery{} buildMutations := func(mutationsAll []*dgoapi.Mutation, queries *gql.GraphQuery, frag []*mutationFragment) []*dgoapi.Mutation { - mutations, err := mutationsFromFragments( + mutations, _ := mutationsFromFragments( frag, func(frag *mutationFragment) ([]byte, error) { return json.Marshal(frag.fragment) @@ -288,9 +439,6 @@ func (mrw *AddRewriter) Rewrite(ctx context.Context, m schema.Mutation) ([]*Upse return nil, nil }) - errs = schema.AppendGQLErrs(errs, schema.GQLWrapf(err, - "failed to rewrite mutation payload")) - mutationsAll = append(mutationsAll, mutations...) qry := queryFromFragments(frag) if qry != nil { @@ -300,103 +448,30 @@ func (mrw *AddRewriter) Rewrite(ctx context.Context, m schema.Mutation) ([]*Upse return mutationsAll } - for _, i := range val { - obj := i.(map[string]interface{}) - frag := rewriteObject(ctx, nil, mutatedType, nil, "", varGen, true, obj, 0, xidMd) - mrw.frags = append(mrw.frags, frag.secondPass) - - mutationsAll = buildMutations(mutationsAll, queries, frag.firstPass) - mutationsAllSec = buildMutations(mutationsAllSec, queriesSec, frag.secondPass) - } + mutationsAll = buildMutations(mutationsAll, queries, fragments) if len(queries.Children) == 0 { queries = nil } - if len(queriesSec.Children) == 0 { - queriesSec = nil - } - + // newNodes is map from variable name to node type. This is used for applying auth on newly added nodes. + // This is collated from newNodes of each fragment. + // Example + // newNodes["Project3"] = schema.Type(Project) newNodes := make(map[string]schema.Type) - for _, f := range mrw.frags { - // squashFragments puts all the new nodes into the first fragment, so we only - // need to collect from there. - copyTypeMap(f[0].newNodes, newNodes) + for _, frag := range fragments { + copyTypeMap(frag.newNodes, newNodes) } - result := []*UpsertMutation{} - if len(mutationsAll) > 0 { - result = append(result, &UpsertMutation{ + ret = append(ret, &UpsertMutation{ Query: []*gql.GraphQuery{queries}, Mutations: mutationsAll, - }) - } - - if len(mutationsAllSec) > 0 { - result = append(result, &UpsertMutation{ - Query: []*gql.GraphQuery{queriesSec}, - Mutations: mutationsAllSec, NewNodes: newNodes, }) } - return result, errs -} - -// FromMutationResult rewrites the query part of a GraphQL add mutation into a Dgraph query. -func (mrw *AddRewriter) FromMutationResult( - ctx context.Context, - mutation schema.Mutation, - assigned map[string]string, - result map[string]interface{}) ([]*gql.GraphQuery, error) { - - var errs error - - uids := make([]uint64, 0) - - for _, frag := range mrw.frags { - err := checkResult(frag, result) - errs = schema.AppendGQLErrs(errs, err) - if err != nil { - continue - } - - node := strings.TrimPrefix(frag[0]. - fragment.(map[string]interface{})["uid"].(string), "_:") - val, ok := assigned[node] - if !ok { - continue - } - uid, err := strconv.ParseUint(val, 0, 64) - if err != nil { - errs = schema.AppendGQLErrs(errs, schema.GQLWrapf(err, - "received %s as an assigned uid from Dgraph,"+ - " but couldn't parse it as uint64", - assigned[node])) - } - - uids = append(uids, uid) - } - - if len(assigned) == 0 && errs == nil { - errs = schema.AsGQLErrors(errors.Errorf("no new node was created")) - } - - customClaims, err := authorization.ExtractCustomClaims(ctx) - if err != nil { - return nil, err - } - - authRw := &authRewriter{ - authVariables: customClaims.AuthVariables, - varGen: NewVariableGenerator(), - selector: queryAuthSelector, - parentVarName: mutation.MutatedType().Name() + "Root", - } - authRw.hasAuthRules = hasAuthRules(mutation.QueryField(), authRw) - - return rewriteAsQueryByIds(mutation.QueryField(), uids, authRw), errs + return ret, retErrors } // Rewrite rewrites set and remove update patches into dql upsert mutations. @@ -423,23 +498,28 @@ func (mrw *AddRewriter) FromMutationResult( // See AddRewriter for how the set and remove fragments get created. func (urw *UpdateRewriter) Rewrite( ctx context.Context, - m schema.Mutation) ([]*UpsertMutation, error) { - + m schema.Mutation, + idExistence map[string]string) ([]*UpsertMutation, error) { mutatedType := m.MutatedType() + varGen := urw.VarGen + xidMetadata := urw.XidMetadata + inp := m.ArgValue(schema.InputArgName).(map[string]interface{}) setArg := inp["set"] delArg := inp["remove"] - if setArg == nil && delArg == nil { - return nil, nil - } + // ret stores a slice of Upsert Mutations. These are used in executing upsert queries in graphql/resolve/mutation.go + var ret []*UpsertMutation + // fragments stores a slice of mutationFragments. This is used in constructing mutationsAll which is returned back to the caller + // of this function as UpsertMutation.mutation + var setFrag, delFrag []*mutationFragment - varGen := NewVariableGenerator() + var retErrors error customClaims, err := authorization.ExtractCustomClaims(ctx) if err != nil { - return nil, err + return ret, err } authRw := &authRewriter{ @@ -453,8 +533,44 @@ func (urw *UpdateRewriter) Rewrite( upsertQuery := RewriteUpsertQueryFromMutation(m, authRw) srcUID := MutationQueryVarUID - xidMd := newXidMetadata() - var errs error + if setArg == nil && delArg == nil { + return ret, nil + } + + if setArg != nil { + obj := setArg.(map[string]interface{}) + fragment, errs := rewriteObject(ctx, mutatedType, nil, srcUID, varGen, obj, xidMetadata, idExistence, UpdateWithSet) + if len(errs) > 0 { + var gqlErrors x.GqlErrorList + for _, err := range errs { + gqlErrors = append(gqlErrors, schema.AsGQLErrors(err)...) + } + retErrors = schema.AppendGQLErrs(retErrors, schema.GQLWrapf(gqlErrors, + "failed to rewrite mutation payload")) + } + if fragment != nil { + setFrag = append(setFrag, fragment) + urw.setFrags = append(urw.setFrags, fragment) + } + } + + if delArg != nil { + obj := delArg.(map[string]interface{}) + // Set additional deletes to false + fragment, errs := rewriteObject(ctx, mutatedType, nil, srcUID, varGen, obj, xidMetadata, idExistence, UpdateWithRemove) + if len(errs) > 0 { + var gqlErrors x.GqlErrorList + for _, err := range errs { + gqlErrors = append(gqlErrors, schema.AsGQLErrors(err)...) + } + retErrors = schema.AppendGQLErrs(retErrors, schema.GQLWrapf(gqlErrors, + "failed to rewrite mutation payload")) + } + if fragment != nil { + delFrag = append(delFrag, fragment) + urw.delFrags = append(urw.delFrags, fragment) + } + } buildMutation := func(setFrag, delFrag []*mutationFragment) *UpsertMutation { var mutSet, mutDel []*dgoapi.Mutation @@ -475,8 +591,7 @@ func (urw *UpdateRewriter) Rewrite( return nil, nil }) - urw.setFrags = append(urw.setFrags, setFrag...) - errs = schema.AppendGQLErrs(errs, errSet) + retErrors = schema.AppendGQLErrs(retErrors, errSet) q1 := queryFromFragments(setFrag) if q1 != nil { @@ -496,8 +611,7 @@ func (urw *UpdateRewriter) Rewrite( return json.Marshal(frag.fragment) }) - urw.delFrags = append(urw.delFrags, delFrag...) - errs = schema.AppendGQLErrs(errs, errDel) + retErrors = schema.AppendGQLErrs(retErrors, errDel) q2 := queryFromFragments(delFrag) if q2 != nil { @@ -520,36 +634,65 @@ func (urw *UpdateRewriter) Rewrite( } } - var setFragF, setFragS, delFragF, delFragS []*mutationFragment + mutations := buildMutation(setFrag, delFrag) + ret = append(ret, mutations) - if setArg != nil { - setFrag := rewriteObject(ctx, nil, mutatedType, nil, srcUID, varGen, true, - setArg.(map[string]interface{}), 0, xidMd) + return ret, retErrors +} - setFragF = setFrag.firstPass - setFragS = setFrag.secondPass - } +// FromMutationResult rewrites the query part of a GraphQL add mutation into a Dgraph query. +func (mrw *AddRewriter) FromMutationResult( + ctx context.Context, + mutation schema.Mutation, + assigned map[string]string, + result map[string]interface{}) ([]*gql.GraphQuery, error) { - if delArg != nil { - delFrag := rewriteObject(ctx, nil, mutatedType, nil, srcUID, varGen, false, - delArg.(map[string]interface{}), 0, xidMd) - delFragF = delFrag.firstPass - delFragS = delFrag.secondPass + var errs error + + uids := make([]uint64, 0) + + for _, frag := range mrw.frags { + err := checkResult(frag, result) + errs = schema.AppendGQLErrs(errs, err) + if err != nil { + continue + } + + node := strings.TrimPrefix(frag[0]. + fragment.(map[string]interface{})["uid"].(string), "_:") + val, ok := assigned[node] + if !ok { + continue + } + uid, err := strconv.ParseUint(val, 0, 64) + if err != nil { + errs = schema.AppendGQLErrs(errs, schema.GQLWrapf(err, + "received %s as an assigned uid from Dgraph,"+ + " but couldn't parse it as uint64", + assigned[node])) + } + + uids = append(uids, uid) } - result := []*UpsertMutation{} + if len(assigned) == 0 && errs == nil { + errs = schema.AsGQLErrors(errors.Errorf("no new node was created")) + } - firstPass := buildMutation(setFragF, delFragF) - if len(firstPass.Mutations) > 0 { - result = append(result, firstPass) + customClaims, err := authorization.ExtractCustomClaims(ctx) + if err != nil { + return nil, err } - secondPass := buildMutation(setFragS, delFragS) - if len(secondPass.Mutations) > 0 { - result = append(result, secondPass) + authRw := &authRewriter{ + authVariables: customClaims.AuthVariables, + varGen: NewVariableGenerator(), + selector: queryAuthSelector, + parentVarName: mutation.MutatedType().Name() + "Root", } + authRw.hasAuthRules = hasAuthRules(mutation.QueryField(), authRw) - return result, schema.GQLWrapf(errs, "failed to rewrite mutation payload") + return rewriteAsQueryByIds(mutation.QueryField(), uids, authRw), errs } // FromMutationResult rewrites the query part of a GraphQL update mutation into a Dgraph query. @@ -750,7 +893,8 @@ func removeNodeReference(m schema.Mutation, authRw *authRewriter, func (drw *deleteRewriter) Rewrite( ctx context.Context, - m schema.Mutation) ([]*UpsertMutation, error) { + m schema.Mutation, + idExistence map[string]string) ([]*UpsertMutation, error) { if m.MutationType() != schema.DeleteMutation { return nil, errors.Errorf( @@ -758,8 +902,6 @@ func (drw *deleteRewriter) Rewrite( m.MutationType()) } - varGen := NewVariableGenerator() - customClaims, err := authorization.ExtractCustomClaims(ctx) if err != nil { return nil, err @@ -767,7 +909,7 @@ func (drw *deleteRewriter) Rewrite( authRw := &authRewriter{ authVariables: customClaims.AuthVariables, - varGen: varGen, + varGen: drw.VarGen, selector: deleteAuthSelector, parentVarName: m.MutatedType().Name() + "Root", } @@ -791,9 +933,12 @@ func (drw *deleteRewriter) Rewrite( if queryField := m.QueryField(); queryField.SelectionSet() != nil { queryAuthRw := &authRewriter{ authVariables: customClaims.AuthVariables, - varGen: varGen, + varGen: drw.VarGen, selector: queryAuthSelector, filterByUid: true, + parentVarName: drw.VarGen.Next(queryField.Type(), "", "", false), + varName: MutationQueryVar, + hasAuthRules: hasAuthRules(queryField, authRw), } queryAuthRw.parentVarName = queryAuthRw.varGen.Next(queryField.Type(), "", "", queryAuthRw.isWritingAuth) @@ -825,6 +970,18 @@ func (drw *deleteRewriter) FromMutationResult( return nil, nil } +// RewriteQueries on deleteRewriter does not return any queries. queries to check +// existence of nodes are not needed as part of Delete Mutation. +// The function generates VarGen and XidMetadata which are used in Rewrite function. +func (drw *deleteRewriter) RewriteQueries( + ctx context.Context, + m schema.Mutation) ([]*gql.GraphQuery, error) { + + drw.VarGen = NewVariableGenerator() + + return []*gql.GraphQuery{}, nil +} + func asUID(val interface{}) (uint64, error) { if val == nil { return 0, errors.Errorf("ID value was null") @@ -924,20 +1081,92 @@ func queryFromFragments(frags []*mutationFragment) *gql.GraphQuery { return qry } -type mutationRes struct { - firstPass []*mutationFragment - secondPass []*mutationFragment -} - -// rewriteObject rewrites obj to a list of mutation fragments. See AddRewriter.Rewrite -// for a description of what those fragments look like. -// -// GraphQL validation has already ensured that the types of arguments (or variables) -// are correct and has ensured that non-nullables are not null. But for deep mutations -// that's not quite enough, and we have add some extra checking on the reference -// types. -// -// Currently adds enforce the schema ! restrictions, but updates don't. +func checkXIDExistsQuery(xidVariable, xidString, xidPredicate string, typ schema.Type) *gql.GraphQuery { + qry := &gql.GraphQuery{ + Attr: xidVariable, + Func: &gql.Function{ + Name: "eq", + Args: []gql.Arg{ + {Value: typ.DgraphPredicate(xidPredicate)}, + {Value: maybeQuoteArg("eq", xidString)}, + }, + }, + Children: []*gql.GraphQuery{{Attr: "uid"}}, + } + addTypeFilter(qry, typ) + return qry +} + +func checkUIDExistsQuery( + val interface{}, + srcField schema.FieldDefinition, + variable string) (*gql.GraphQuery, error) { + + uid, err := asUID(val) + if err != nil { + return nil, err + } + + query := &gql.GraphQuery{ + Attr: variable, + UID: []uint64{uid}, + Children: []*gql.GraphQuery{{Attr: "uid"}}, + } + addTypeFilter(query, srcField.Type()) + addUIDFunc(query, []uint64{uid}) + return query, nil +} + +// asIDReference makes a mutation fragment that resolves a reference to the uid in val. There's +// a bit of extra mutation to build if the original mutation contains a reference to +// another node: e.g it was say adding a Post with: +// { "title": "...", "author": { "id": "0x123" }, ... } +// and we'd gotten to here ^^ +// in rewriteObject with srcField = "author" srcUID = "XYZ" +// and the schema says that Post.author and Author.Posts are inverses of each other, then we need +// to make sure that inverse link is added/removed. We have to make sure the Dgraph upsert +// mutation ends up like: +// +// mutation : +// { "uid": "XYZ", "title": "...", "author": { "id": "0x123", "posts": [ { "uid": "XYZ" } ] }, ... } +// asIDReference builds the fragment +// { "id": "0x123", "posts": [ { "uid": "XYZ" } ] } +func asIDReference( + ctx context.Context, + val interface{}, + srcField schema.FieldDefinition, + srcUID string, + varGen *VariableGenerator, + isRemove bool) *mutationFragment { + + result := make(map[string]interface{}, 2) + frag := newFragment(result) + + // No need to check if this is a valid UID. It is because this would have been checked + // in checkUIDExistsQuery function called from corresponding getExistenceQueries function. + + result["uid"] = val // val will contain the UID string. + + addInverseLink(result, srcField, srcUID) + + // Delete any additional old edges from inverse nodes in case this is not a remove + // as part of an Update Mutation. + if !isRemove { + addAdditionalDeletes(ctx, frag, varGen, srcField, srcUID, val.(string)) + } + return frag + +} + +// rewriteObject rewrites obj to a list of mutation fragments. See AddRewriter.Rewrite +// for a description of what those fragments look like. +// +// GraphQL validation has already ensured that the types of arguments (or variables) +// are correct and has ensured that non-nullables are not null. But for deep mutations +// that's not quite enough, and we have add some extra checking on the reference +// types. +// +// Currently adds enforce the schema ! restrictions, but updates don't. // e.g. a Post might have `title: String!`` in the schema, but, a Post update could // set that to to null. ATM we allow this and it'll just triggers GraphQL error propagation // when that is in a query result. This is the same case as deletes: e.g. deleting @@ -945,436 +1174,521 @@ type mutationRes struct { // (That might actually be helpful if you want to run one mutation to remove something // and then another to correct it.) // -// rewriteObject returns two set of mutations, firstPass and secondPass. We start -// building mutations recursively in the secondPass. Whenever we encounter an XID object, -// we push it to firstPass. We need to make sure that the XID doesn't refer hasInverse links -// to secondPass, and then to make those links ourselves. +// rewriteObject builds a set of mutations. Using the argument idExistence, it is decided +// whether to create new nodes or link to existing nodes. Mutations are built recursively +// in a dfs like algorithm. func rewriteObject( ctx context.Context, - parentTyp schema.Type, typ schema.Type, srcField schema.FieldDefinition, srcUID string, varGen *VariableGenerator, - withAdditionalDeletes bool, obj map[string]interface{}, - deepXID int, - xidMetadata *xidMetadata) *mutationRes { + xidMetadata *xidMetadata, + idExistence map[string]string, + mutationType MutationType) (*mutationFragment, []error) { - atTopLevel := srcField == nil - topLevelAdd := srcUID == "" + // There could be the following cases: + // 1. We need to create a new node. + // 2. We use an existing node and link it to the parent. + // We may have to add an inverse edge in this case. But generally, no other amendments + // to the node need to be done. + // Note that as similar traversal of input tree was carried with getExistenceQueries, we + // don't have to report the same errors. - variable := varGen.Next(typ, "", "", false) + atTopLevel := srcField == nil + var retErrors []error + variable := "" id := typ.IDField() if id != nil { + // Check if the ID field is referenced in the mutation if idVal, ok := obj[id.Name()]; ok { - if idVal != nil { - return &mutationRes{secondPass: []*mutationFragment{ - asIDReference(ctx, idVal, srcField, srcUID, variable, - withAdditionalDeletes, varGen)}} + // This node is referenced and must definitely exist. + // If it does not exist, we should be throwing an error. + // No need to add query if the UID is already been seen. + + // Fetch corresponding variable name + variable = varGen.Next(typ, id.Name(), idVal.(string), false) + + // Get whether UID exists or not from existenceQueriesResult + if _, ok := idExistence[variable]; ok { + // UID exists. + // We return an error if this is at toplevel. Else, we return the ID reference + if atTopLevel { + // We need to conceal the error because we might be leaking information to the user if it + // tries to add duplicate data to the field with @id. + var err error + if queryAuthSelector(typ) == nil { + err = x.GqlErrorf("id %s already exists for type %s", idVal.(string), typ.Name()) + } else { + // This error will only be reported in debug mode. + err = x.GqlErrorf("GraphQL debug: id already exists for type %s", typ.Name()) + } + retErrors = append(retErrors, err) + return nil, retErrors + } else { + return asIDReference(ctx, idVal, srcField, srcUID, varGen, mutationType == UpdateWithRemove), nil + } + } else { + // Reference UID does not exist. This is an error. + err := errors.Errorf("ID \"%s\" isn't a %s", idVal.(string), srcField.Type().Name()) + retErrors = append(retErrors, err) + return nil, retErrors } - delete(obj, id.Name()) } } - var xidFrag *mutationFragment - var xidString string xid := typ.XIDField() - xidEncounteredFirstTime := false + var xidString string if xid != nil { if xidVal, ok := obj[xid.Name()]; ok && xidVal != nil { xidString, ok = xidVal.(string) - if !ok { - errFrag := newFragment(nil) - errFrag.err = errors.New("encountered an XID that isn't a string") - return &mutationRes{secondPass: []*mutationFragment{errFrag}} - } - // if the object has an xid, the variable name will be formed from the xidValue in order - // to handle duplicate object addition/updation variable = varGen.Next(typ, xid.Name(), xidString, false) - // check if an object with same xid has been encountered earlier - if xidMetadata.variableObjMap[variable] != nil { - // if we already encountered an object with same xid earlier, and this object is - // considered a duplicate of the existing object, then return error. - if xidMetadata.isDuplicateXid(atTopLevel, variable, obj, srcField) { - errFrag := newFragment(nil) - errFrag.err = errors.Errorf("duplicate XID found: %s", xidString) - return &mutationRes{secondPass: []*mutationFragment{errFrag}} + // Three cases: + // 1. If the queryResult UID exists. Add a reference. + // 2. If the queryResult UID does not exist and this is the first time we are seeing + // this. Then, return error. + // 3. The queryResult UID does not exist. But, this could be a reference to an XID + // node added during the mutation rewriting. This is handled by adding the new blank UID + // to existenceQueryResult. + + // Get whether node with XID exists or not from existenceQueriesResult + if uid, ok := idExistence[variable]; ok { + // node with XID exists. This is a reference. + // We return an error if this is at toplevel. Else, we return the ID reference + if atTopLevel { + // We need to conceal the error because we might be leaking information to the user if it + // tries to add duplicate data to the field with @id. + var err error + if queryAuthSelector(typ) == nil { + err = x.GqlErrorf("id %s already exists for type %s", xidString, typ.Name()) + } else { + // This error will only be reported in debug mode. + err = x.GqlErrorf("GraphQL debug: id already exists for type %s", typ.Name()) + } + retErrors = append(retErrors, err) + return nil, retErrors + } else { + return asIDReference(ctx, uid, srcField, srcUID, varGen, mutationType == UpdateWithRemove), nil } } else { - // if not encountered till now, add it to the map - xidMetadata.variableObjMap[variable] = obj - xidEncounteredFirstTime = true - } - // save if this variable was seen at top level - if !xidMetadata.seenAtTopLevel[variable] { - xidMetadata.seenAtTopLevel[variable] = atTopLevel - } - } - - deepXID += 1 - } - - var parentFrags []*mutationFragment - - if !atTopLevel { // top level is never a reference - it's a new addition. - // this is the case of a lower level having xid which is a reference. - if xid != nil && xidString != "" { - xidFrag = asXIDReference(ctx, srcField, srcUID, typ, xid.Name(), xidString, - variable, withAdditionalDeletes, varGen, xidMetadata) - - // Inverse Link is added as a Part of asXIDReference so we delete any provided - // Link to the object. - // Example: for this mutation - // mutation addCountry($inp: AddCountryInput!) { - // addCountry(input: [$inp]) { - // country { - // id - // } - // } - // } - // with the input: - //{ - // "inp": { - // "name": "A Country", - // "states": [ - // { "code": "abc", "name": "Alphabet" }, - // { "code": "def", "name": "Vowel", "country": { "name": "B country" } } - // ] - // } - // } - // we delete the link of Second state to "B Country" - deleteInverseObject(obj, srcField) - - if deepXID > 2 { - // Here we link the already existing node with an xid to the parent whose id is - // passed in srcUID. We do this linking only if there is a hasInverse relationship - // between the two. - // So for example if we had the addAuthor mutation which is also adding nested - // posts, then we link the authorUid(srcUID) - Author.posts - uid(Post) here. - - res := make(map[string]interface{}, 1) - res["uid"] = srcUID - attachChild(res, parentTyp, srcField, fmt.Sprintf("uid(%s)", variable)) - parentFrag := newFragment(res) - parentFrag.conditions = append(parentFrag.conditions, xidFrag.conditions...) - parentFrags = append(parentFrags, parentFrag) - } - } else if !withAdditionalDeletes { - // In case of delete, id/xid is required - if xid == nil && id == nil { - err := errors.Errorf("object of type: %s doesn't have a field of type ID! "+ - "or @id and can't be referenced for deletion", typ.Name()) - return &mutationRes{secondPass: []*mutationFragment{{err: err}}} + // Node with XID does not exist. It means this is a new node. + // This node will be created later. + exclude := "" + if srcField != nil { + invField := srcField.Inverse() + if invField != nil { + exclude = invField.Name() + } + } + if err := typ.EnsureNonNulls(obj, exclude); err != nil { + // This object does not contain XID. This is an error. + retErrors = append(retErrors, err) + return nil, retErrors + } + // Set existenceQueryResult to _:variable. This is to make referencing to + // this node later easier. + idExistence[variable] = fmt.Sprintf("_:%s", variable) } - var name string - if xid != nil { - name = xid.Name() - } else { - name = id.Name() + } else { + // There are two possibilities here: + // 1. This is an Add Mutation or we are at some deeper level inside Update Mutation: + // In this case this is an error as XID field if referenced anywhere inside Add Mutation + // or at deeper levels in Update Mutation has to be present. + // 2. This is an Update Mutation and we are at top level: + // In this case this is not an error as the UID at top level of Update Mutation is + // referenced as uid(x) in mutations. We don't throw an error in this case and continue + // with the function. + if mutationType == Add || !atTopLevel { + err := errors.Errorf("field %s cannot be empty", xid.Name()) + retErrors = append(retErrors, err) + return nil, retErrors } - return &mutationRes{secondPass: invalidObjectFragment(fmt.Errorf("%s is not provided", name), - xidFrag, variable, xidString)} } } - if !atTopLevel && withAdditionalDeletes { - // top level mutations are fully checked by GraphQL validation - exclude := "" - if srcField != nil { - invField := srcField.Inverse() - if invField != nil { - exclude = invField.Name() - } - } - if err := typ.EnsureNonNulls(obj, exclude); err != nil { - // This object is either an invalid deep mutation or it's an xid reference - // and asXIDReference must to apply or it's an error. - return &mutationRes{secondPass: invalidObjectFragment(err, xidFrag, variable, xidString)} - } + // This is not an XID reference. This is also not a UID reference. + // This is definitely a new node. + // Create new node + if variable == "" { + // This will happen in case when this is a new node and does not contain XID. + variable = varGen.Next(typ, "", "", false) } - if !atTopLevel && !withAdditionalDeletes { - // For remove op (!withAdditionalDeletes), we don't need to generate a new - // blank node. - if xidFrag != nil { - return &mutationRes{secondPass: []*mutationFragment{xidFrag}} - } else { - return &mutationRes{} - } - } + // myUID is used for referencing this node. It is set to _:variable + myUID := fmt.Sprintf("_:%s", variable) - var myUID string - newObj := make(map[string]interface{}, len(obj)) + // Assign dgraph.types attribute. + dgraphTypes := []string{typ.DgraphName()} + dgraphTypes = append(dgraphTypes, typ.Interfaces()...) - if !atTopLevel || topLevelAdd { - dgraphTypes := []string{typ.DgraphName()} - dgraphTypes = append(dgraphTypes, typ.Interfaces()...) - newObj["dgraph.type"] = dgraphTypes - myUID = fmt.Sprintf("_:%s", variable) - - if xid == nil || deepXID > 2 { - // If this object had an overwritten value for the inverse field, then we don't want to - // use that value as we will add the link to the inverse field in the below - // function call with the parent of this object - // for example, for this mutation: - // mutation addAuthor($auth: AddAuthorInput!) { - // addAuthor(input: [$auth]) { - // author { - // id - // } - // } - // } - // with the following input - // { - // "auth": { - // "name": "A.N. Author", - // "posts": [ { "postID": "0x456" }, {"title": "New Post", "author": {"name": "Abhimanyu"}} ] - // } - // } - // We delete the link of second input post with Author "name" : "Abhimanyu". - deleteInverseObject(obj, srcField) - - // Lets link the new node that we are creating with the parent if a @hasInverse - // exists between the two. - // So for example if we had the addAuthor mutation which is also adding nested - // posts, then we add the link _:Post Post.author AuthorUID(srcUID) here. - addInverseLink(newObj, srcField, srcUID) + // Create newObj map. This map will be returned as part of mutationFragment. + newObj := make(map[string]interface{}, len(obj)) - } - } else { + if mutationType != Add && atTopLevel { + // It's an update and we are at top level. So, the UID of node(s) for which + // we are rewriting is/are referenced using "uid(x)" as part of mutations. + // We don't need to create a new blank node in this case. + // srcUID is equal to uid(x) in this case. + newObj["uid"] = srcUID myUID = srcUID + } else if mutationType == UpdateWithRemove { + // It's a remove. As remove can only be part of Update Mutation. It can + // be inferred that this is an Update Mutation. + // In case of remove of Update, deeper level nodes have to be referenced by ID + // or XID. If we have reached this stage, we can be sure that no such reference + // to ID or XID exists. In that case, we throw an error. + err := errors.Errorf("id is not provided") + retErrors = append(retErrors, err) + return nil, retErrors + } else { + // We are in Add Mutation or at a deeper level in Update Mutation set. + // If we have reached this stage, we can be sure that we need to create a new + // node as part of the mutation. The new node is referenced as a blank node like + // "_:Project2" . myUID will store the variable generated to reference this node. + newObj["dgraph.type"] = dgraphTypes + newObj["uid"] = myUID } - newObj["uid"] = myUID + // Add Inverse Link if necessary + deleteInverseObject(obj, srcField) + addInverseLink(newObj, srcField, srcUID) + frag := newFragment(newObj) frag.newNodes[variable] = typ - results := &mutationRes{secondPass: []*mutationFragment{frag}} - if xid != nil && !atTopLevel && !xidEncounteredFirstTime && deepXID <= 2 { - // If this is an xid that has been encountered before, e.g. think add mutations with - // multiple objects as input. In that case we don't need to add the fragment to create this - // object, so we clear it out. We do need other fragments for linking this node to its - // parent which are added later. - // If deepXID > 2 then even if the xid has been encountered before we still keep it and - // build its mutation to cover all possible scenarios. - results.secondPass = results.secondPass[:0] - } - - // if xidString != "", then we are adding with an xid. In which case, we have to ensure - // as part of the upsert that the xid doesn't already exist. - if xidString != "" { - if atTopLevel && !xidMetadata.queryExists[variable] { - // If not at top level, the query is already added by asXIDReference - frag.queries = []*gql.GraphQuery{ - xidQuery(variable, xidString, xid.Name(), typ), + updateFromChildren := func(parentFragment, childFragment *mutationFragment) { + copyTypeMap(childFragment.newNodes, parentFragment.newNodes) + frag.queries = append(parentFragment.queries, childFragment.queries...) + frag.deletes = append(parentFragment.deletes, childFragment.deletes...) + frag.check = func(lcheck, rcheck resultChecker) resultChecker { + return func(m map[string]interface{}) error { + return schema.AppendGQLErrs(lcheck(m), rcheck(m)) } - xidMetadata.queryExists[variable] = true - } - frag.conditions = []string{fmt.Sprintf("eq(len(%s), 0)", variable)} + }(parentFragment.check, childFragment.check) + } - // We need to conceal the error because we might be leaking information to the user if it - // tries to add duplicate data to the field with @id. - var err error - if queryAuthSelector(typ) == nil { - err = x.GqlErrorf("id %s already exists for type %s", xidString, typ.Name()) - } else { - // This error will only be reported in debug mode. - err = x.GqlErrorf("GraphQL debug: id already exists for type %s", typ.Name()) - } - frag.check = checkQueryResult(variable, err, nil) + // Iterate on fields and call the same function recursively. + var fields []string + for field := range obj { + fields = append(fields, field) } + // Fields are sorted to ensure that they are traversed in specific order each time. Golang maps + // don't store keys in sorted order. + sort.Strings(fields) + for _, field := range fields { + val := obj[field] + + fieldDef := typ.Field(field) + fieldName := typ.DgraphPredicate(field) - if xid != nil && !atTopLevel { - if deepXID <= 2 { // elements in firstPass or not - // duplicate query in elements >= 2, as the pair firstPass element would already have - // the same query. - frag.queries = []*gql.GraphQuery{ - xidQuery(variable, xidString, xid.Name(), typ), + // This fixes mutation when dgraph predicate has special characters. PR #5526 + if strings.HasPrefix(fieldName, "<") && strings.HasSuffix(fieldName, ">") { + fieldName = fieldName[1 : len(fieldName)-1] + } + + // TODO: Write a function for aggregating data of fragment from child nodes. + switch val := val.(type) { + case map[string]interface{}: + if fieldDef.Type().IsUnion() { + fieldMutationFragment, err := rewriteUnionField(ctx, fieldDef, myUID, varGen, val, xidMetadata, idExistence, mutationType) + if fieldMutationFragment != nil { + newObj[fieldName] = fieldMutationFragment.fragment + updateFromChildren(frag, fieldMutationFragment) + } + retErrors = append(retErrors, err...) + } else if fieldDef.Type().IsGeo() { + newObj[fieldName] = + map[string]interface{}{ + "type": fieldDef.Type().Name(), + "coordinates": rewriteGeoObject(val, fieldDef.Type()), + } + } else { + fieldMutationFragment, err := rewriteObject(ctx, fieldDef.Type(), fieldDef, myUID, varGen, val, xidMetadata, idExistence, mutationType) + if fieldMutationFragment != nil { + newObj[fieldName] = fieldMutationFragment.fragment + updateFromChildren(frag, fieldMutationFragment) + } + retErrors = append(retErrors, err...) } - } else { - // We need to link the parent to the element we are just creating - res := make(map[string]interface{}, 1) - res["uid"] = srcUID - this := fmt.Sprintf("_:%s", variable) - attachChild(res, parentTyp, srcField, this) - - parentFrag := newFragment(res) - parentFrag.conditions = append(parentFrag.conditions, frag.conditions...) - parentFrags = append(parentFrags, parentFrag) + case []interface{}: + mutationFragments := make([]interface{}, 0) + var fieldMutationFragment *mutationFragment + var err []error + for _, object := range val { + switch object := object.(type) { + case map[string]interface{}: + if fieldDef.Type().IsUnion() { + fieldMutationFragment, err = rewriteUnionField(ctx, fieldDef, myUID, varGen, object, xidMetadata, idExistence, mutationType) + } else if fieldDef.Type().IsGeo() { + fieldMutationFragment = newFragment( + map[string]interface{}{ + "type": fieldDef.Type().Name(), + "coordinates": rewriteGeoObject(object, fieldDef.Type()), + }, + ) + } else { + fieldMutationFragment, err = rewriteObject(ctx, fieldDef.Type(), fieldDef, myUID, varGen, object, xidMetadata, idExistence, mutationType) + } + if fieldMutationFragment != nil { + mutationFragments = append(mutationFragments, fieldMutationFragment.fragment) + updateFromChildren(frag, fieldMutationFragment) + } + retErrors = append(retErrors, err...) + default: + // This is a scalar list. + mutationFragments = append(mutationFragments, object) + } + + } + if newObj[fieldName] != nil { + newObj[fieldName] = append(newObj[fieldName].([]interface{}), mutationFragments...) + } else { + newObj[fieldName] = mutationFragments + } + default: + // This field is either a scalar value or a null. + newObj[fieldName] = val } } - var childrenFirstPass []*mutationFragment - // we build the mutation to add object here. If XID != nil, we would then move it to - // firstPass from secondPass (frag). + return frag, retErrors +} - // if this object has an xid, then we don't need to - // rewrite its children if we have encountered it earlier. +// existenceQueries takes a GraphQL JSON object as obj and creates queries to find +// out if referenced nodes by XID and UID exist or not. +// This is done in recursive fashion using a dfs. +// This function is called from RewriteQueries function on AddRewriter and UpdateRewriter +// objects. +// Look at description of RewriteQueries for an example of generated existence queries. +func existenceQueries( + ctx context.Context, + typ schema.Type, + srcField schema.FieldDefinition, + varGen *VariableGenerator, + obj map[string]interface{}, + xidMetadata *xidMetadata) ([]*gql.GraphQuery, []error) { - // For deepXIDs even if the xid has been encountered before, we should build the mutation for - // this object. - if xidString == "" || xidEncounteredFirstTime || deepXID > 2 { - var fields []string - for field := range obj { - fields = append(fields, field) - } - sort.Strings(fields) + atTopLevel := srcField == nil + var ret []*gql.GraphQuery + var retErrors []error - for _, field := range fields { - val := obj[field] - var frags *mutationRes + id := typ.IDField() + if id != nil { + // Check if the ID field is referenced in the mutation + if idVal, ok := obj[id.Name()]; ok { + if idVal != nil { + // No need to add query if the UID is already been seen. + if xidMetadata.seenUIDs[idVal.(string)] == true { + return ret, retErrors + } + // Mark this UID as seen. + xidMetadata.seenUIDs[idVal.(string)] = true + variable := varGen.Next(typ, id.Name(), idVal.(string), false) - fieldDef := typ.Field(field) - fieldName := typ.DgraphPredicate(field) + query, err := checkUIDExistsQuery(idVal, srcField, variable) - // This fixes mutation when dgraph predicate has special characters. PR #5526 - if strings.HasPrefix(fieldName, "<") && strings.HasSuffix(fieldName, ">") { - fieldName = fieldName[1 : len(fieldName)-1] + if err != nil { + retErrors = append(retErrors, err) + } + ret = append(ret, query) + return ret, retErrors + // Add check UID query and return it. + // There is no need to move forward. If reference ID field is given, + // it has to exist. } + // As the type has not been referenced by ID field, remove it so that it does + // not interfere with further processing. + delete(obj, id.Name()) + } + } - switch val := val.(type) { - case map[string]interface{}: - if fieldDef.Type().IsUnion() { - frags = rewriteUnionField(ctx, typ, fieldDef, myUID, varGen, - withAdditionalDeletes, val, deepXID, xidMetadata, -1) - } else if fieldDef.Type().IsGeo() { - frags = &mutationRes{ - secondPass: []*mutationFragment{ - newFragment( - map[string]interface{}{ - "type": fieldDef.Type().Name(), - "coordinates": rewriteGeoObject(val, fieldDef.Type()), - }, - ), - }, - } - } else { - // This field is another GraphQL object, which could either be linking to an - // existing node by it's ID - // { "title": "...", "author": { "id": "0x123" } - // like here ^^ - // or giving the data to create the object as part of a deep mutation - // { "title": "...", "author": { "username": "new user", "dob": "...", ... } - // like here ^^ - frags = - rewriteObject(ctx, typ, fieldDef.Type(), fieldDef, myUID, varGen, - withAdditionalDeletes, val, deepXID, xidMetadata) + xid := typ.XIDField() + var xidString string + if xid != nil { + if xidVal, ok := obj[xid.Name()]; ok && xidVal != nil { + switch xid.Type().Name() { + case "Int": + val, ok := xidVal.(int64) + if !ok { + retErrors = append(retErrors, errors.New(fmt.Sprintf("encountered an XID %s with %s that isn't "+ + "a Int but data type in schema is Int", xid.Name(), xid.Type().Name()))) + return nil, retErrors } - - case []interface{}: - // This field is either: - // 1) A list of objects: e.g. if the schema said `categories: [Categories]` - // Which can be references to existing objects - // { "title": "...", "categories": [ { "id": "0x123" }, { "id": "0x321" }, ...] } - // like here ^^ ^^ - // Or a deep mutation that creates new objects - // { "title": "...", "categories": [ { "name": "new category", ... }, ... ] } - // like here ^^ ^^ - // 2) Or a list of scalars - e.g. if schema said `scores: [Float]` - // { "title": "...", "scores": [10.5, 9.3, ... ] - // like here ^^ - frags = - rewriteList(ctx, typ, fieldDef.Type(), fieldDef, myUID, varGen, - withAdditionalDeletes, val, deepXID, xidMetadata) + xidString = strconv.FormatInt(val, 10) + case "Float": + val, ok := xidVal.(float64) + if !ok { + retErrors = append(retErrors, errors.New(fmt.Sprintf("encountered an XID %s with %s that isn't "+ + "a Float but data type in schema is Float", xid.Name(), xid.Type().Name()))) + return nil, retErrors + } + xidString = strconv.FormatFloat(val, 'f', -1, 64) + case "Int64": + fallthrough default: - // This field is either: - // 1) a scalar value: e.g. - // { "title": "My Post", ... } - // 2) a JSON null: e.g. - // { "text": null, ... } - // e.g. to remove the text or - // { "friends": null, ... } - // to remove all friends - - // Fields with `id` directive cannot have empty values. - if fieldDef.HasIDDirective() && val == "" { - errFrag := newFragment(nil) - errFrag.err = fmt.Errorf("encountered an empty value for @id field `%s`", fieldName) - return &mutationRes{secondPass: []*mutationFragment{errFrag}} + xidString, ok = xidVal.(string) + if !ok { + retErrors = append(retErrors, errors.New(fmt.Sprintf("encountered an XID %s with %s that isn't "+ + "a String or Int64", xid.Name(), xid.Type().Name()))) + return nil, retErrors + } + } + variable := varGen.Next(typ, xid.Name(), xidString, false) + + if xidMetadata.variableObjMap[variable] != nil { + // if we already encountered an object with same xid earlier, and this object is + // considered a duplicate of the existing object, then return error. + if xidMetadata.isDuplicateXid(atTopLevel, variable, obj, srcField) { + err := errors.Errorf("duplicate XID found: %s", xidString) + retErrors = append(retErrors, err) + return nil, retErrors } - frags = &mutationRes{secondPass: []*mutationFragment{newFragment(val)}} + // In the other case it is not duplicate. In this case we don't move ahead and + // stop processing. + return ret, retErrors } - childrenFirstPass = appendFragments(childrenFirstPass, frags.firstPass) - results.secondPass = squashFragments(squashIntoObject(fieldName), results.secondPass, - frags.secondPass) + // if not encountered till now, add it to the map + xidMetadata.variableObjMap[variable] = obj + + // save if this node was seen at top level. + if !xidMetadata.seenAtTopLevel[variable] { + xidMetadata.seenAtTopLevel[variable] = atTopLevel + } + + query := checkXIDExistsQuery(variable, xidString, xid.Name(), typ) + + ret = append(ret, query) + // Don't return just over here as there maybe more nodes in the children tree. } } - // In the case of an XID, move the secondPass (creation mutation) to firstPass - if xid != nil && !atTopLevel { - results.firstPass = appendFragments(results.firstPass, results.secondPass) - results.secondPass = []*mutationFragment{} + // Iterate on fields and call the same function recursively. + var fields []string + for field := range obj { + fields = append(fields, field) } + // Fields are sorted to ensure that they are traversed in specific order each time. Golang maps + // don't store keys in sorted order. + sort.Strings(fields) + for _, field := range fields { + val := obj[field] - // add current conditions to all the new fragments from children. - // childrens should only be addded when this level is true - conditions := []string{} - for _, i := range results.firstPass { - conditions = append(conditions, i.conditions...) - } + fieldDef := typ.Field(field) + fieldName := typ.DgraphPredicate(field) - for _, i := range childrenFirstPass { - i.conditions = append(i.conditions, conditions...) - } - results.firstPass = appendFragments(results.firstPass, childrenFirstPass) + // This fixes mutation when dgraph predicate has special characters. PR #5526 + if strings.HasPrefix(fieldName, "<") && strings.HasSuffix(fieldName, ">") { + fieldName = fieldName[1 : len(fieldName)-1] + } - // parentFrags are reverse links to parents. only applicable for when deepXID > 2 - results.firstPass = appendFragments(results.firstPass, parentFrags) + switch val := val.(type) { + case map[string]interface{}: + if fieldDef.Type().IsUnion() { + fieldQueries, err := existenceQueriesUnion(ctx, typ, fieldDef, varGen, val, xidMetadata, -1) + retErrors = append(retErrors, err...) + ret = append(ret, fieldQueries...) + } else { + fieldQueries, err := existenceQueries(ctx, fieldDef.Type(), fieldDef, varGen, val, xidMetadata) + retErrors = append(retErrors, err...) + ret = append(ret, fieldQueries...) + } + case []interface{}: + for i, object := range val { + switch object := object.(type) { + case map[string]interface{}: + var fieldQueries []*gql.GraphQuery + var err []error + if fieldDef.Type().IsUnion() { + fieldQueries, err = existenceQueriesUnion(ctx, typ, fieldDef, varGen, object, xidMetadata, i) + } else { + fieldQueries, err = existenceQueries(ctx, fieldDef.Type(), fieldDef, varGen, object, xidMetadata) + } + retErrors = append(retErrors, err...) + ret = append(ret, fieldQueries...) + default: + // This is a scalar list. So, it won't contain any XID. + // Don't do anything. + } - // xidFrag contains the mutation to update object if it is present. - // add it to secondPass if deepXID <= 2, otherwise firstPass for relevant hasInverse links. - if xidFrag != nil && deepXID > 2 { - results.firstPass = appendFragments(results.firstPass, []*mutationFragment{xidFrag}) - } else if xidFrag != nil { - results.secondPass = appendFragments(results.secondPass, []*mutationFragment{xidFrag}) + } + default: + // This field is either a scalar value or a null. + // Fields with ID directive cannot have empty values. Checking it here. + if fieldDef.HasIDDirective() && val == "" { + err := fmt.Errorf("encountered an empty value for @id field `%s`", fieldName) + retErrors = append(retErrors, err) + return nil, retErrors + } + } } - return results + return ret, retErrors } -// if this is a union field, then obj should have only one key which will be a ref -// to one of the member types. Eg: -// { "dogRef" : { ... } } -// So, just rewrite it as an object with correct underlying type. -func rewriteUnionField(ctx context.Context, +func existenceQueriesUnion( + ctx context.Context, parentTyp schema.Type, srcField schema.FieldDefinition, - srcUID string, varGen *VariableGenerator, - withAdditionalDeletes bool, obj map[string]interface{}, - deepXID int, xidMetadata *xidMetadata, - listIndex int) *mutationRes { + listIndex int) ([]*gql.GraphQuery, []error) { + + var retError []error if len(obj) != 1 { - errFrag := newFragment(nil) + var err error // if this was called from rewriteList, // the listIndex will tell which particular item in the list has an error. if listIndex >= 0 { - errFrag.err = fmt.Errorf( + err = fmt.Errorf( "value for field `%s` in type `%s` index `%d` must have exactly one child, "+ "found %d children", srcField.Name(), parentTyp.Name(), listIndex, len(obj)) } else { - errFrag.err = fmt.Errorf( + err = fmt.Errorf( "value for field `%s` in type `%s` must have exactly one child, found %d children", srcField.Name(), parentTyp.Name(), len(obj)) } - return &mutationRes{secondPass: []*mutationFragment{errFrag}} + retError = append(retError, err) + return nil, retError } - var typ schema.Type + var newtyp schema.Type for memberRef, memberRefVal := range obj { memberTypeName := strings.ToUpper(memberRef[:1]) + memberRef[1:len( memberRef)-3] srcField = srcField.WithMemberType(memberTypeName) - typ = srcField.Type() + newtyp = srcField.Type() obj = memberRefVal.(map[string]interface{}) } - return rewriteObject(ctx, parentTyp, typ, srcField, srcUID, varGen, - withAdditionalDeletes, obj, deepXID, xidMetadata) + return existenceQueries(ctx, newtyp, srcField, varGen, obj, xidMetadata) +} + +// if this is a union field, then obj should have only one key which will be a ref +// to one of the member types. Eg: +// { "dogRef" : { ... } } +// So, just rewrite it as an object with correct underlying type. +func rewriteUnionField( + ctx context.Context, + srcField schema.FieldDefinition, + srcUID string, + varGen *VariableGenerator, + obj map[string]interface{}, + xidMetadata *xidMetadata, + existenceQueriesResult map[string]string, + mutationType MutationType) (*mutationFragment, []error) { + + var newtyp schema.Type + for memberRef, memberRefVal := range obj { + memberTypeName := strings.ToUpper(memberRef[:1]) + memberRef[1:len( + memberRef)-3] + srcField = srcField.WithMemberType(memberTypeName) + newtyp = srcField.Type() + obj = memberRefVal.(map[string]interface{}) + } + return rewriteObject(ctx, newtyp, srcField, srcUID, varGen, obj, xidMetadata, existenceQueriesResult, mutationType) } // rewriteGeoObject rewrites the given value correctly based on the underlying Geo type. @@ -1436,23 +1750,6 @@ func rewriteMultiPolygon(val map[string]interface{}) []interface{} { return res } -func invalidObjectFragment( - err error, - xidFrag *mutationFragment, - variable, xidString string) []*mutationFragment { - - if xidFrag != nil { - xidFrag.check = - checkQueryResult(variable, - nil, - schema.GQLWrapf(err, - "xid \"%s\" doesn't exist and input object not well formed", xidString)) - - return []*mutationFragment{xidFrag} - } - return []*mutationFragment{{err: err}} -} - func checkQueryResult(qry string, yes, no error) resultChecker { return func(m map[string]interface{}) error { if val, exists := m[qry]; exists && val != nil { @@ -1464,121 +1761,6 @@ func checkQueryResult(qry string, yes, no error) resultChecker { } } -// asIDReference makes a mutation fragment that resolves a reference to the uid in val. There's -// a bit of extra mutation to build if the original mutation contains a reference to -// another node: e.g it was say adding a Post with: -// { "title": "...", "author": { "id": "0x123" }, ... } -// and we'd gotten to here ^^ -// in rewriteObject with srcField = "author" srcUID = "XYZ" -// and the schema says that Post.author and Author.Posts are inverses of each other, then we need -// to make sure that inverse link is added/removed. We have to make sure the Dgraph upsert -// mutation ends up like: -// -// query : -// Author1 as Author1(func: uid(0x123)) @filter(type(Author)) { uid } -// condition : -// len(Author1) > 0 -// mutation : -// { "uid": "XYZ", "title": "...", "author": { "id": "0x123", "posts": [ { "uid": "XYZ" } ] }, ... } -// asIDReference builds the fragment -// { "id": "0x123", "posts": [ { "uid": "XYZ" } ] } -func asIDReference( - ctx context.Context, - val interface{}, - srcField schema.FieldDefinition, - srcUID, variable string, - withAdditionalDeletes bool, - varGen *VariableGenerator) *mutationFragment { - - result := make(map[string]interface{}, 2) - frag := newFragment(result) - - uid, err := asUID(val) - if err != nil { - frag.err = err - return frag - } - - result["uid"] = val - - addInverseLink(result, srcField, srcUID) - - qry := &gql.GraphQuery{ - Var: variable, - Attr: variable, - UID: []uint64{uid}, - Children: []*gql.GraphQuery{{Attr: "uid"}}, - } - addTypeFilter(qry, srcField.Type()) - addUIDFunc(qry, []uint64{uid}) - - frag.queries = []*gql.GraphQuery{qry} - frag.conditions = []string{fmt.Sprintf("eq(len(%s), 1)", variable)} - frag.check = - checkQueryResult(variable, - nil, - errors.Errorf("ID \"%#x\" isn't a %s", uid, srcField.Type().Name())) - - if withAdditionalDeletes { - addAdditionalDeletes(ctx, frag, varGen, srcField, srcUID, variable) - } - - return frag -} - -// asXIDReference makes a mutation fragment that resolves a reference to an XID. There's -// a bit of extra mutation to build since if the original mutation contains a reference to -// another node, e.g it was say adding a Post with: -// { "title": "...", "author": { "username": "A-user" }, ... } -// and we'd gotten to here ^^ -// in rewriteObject with srcField = "author" srcUID = "XYZ" -// and the schema says that Post.author and Author.Posts are inverses of each other, then we need -// to make sure that inverse link is added/removed. We have to make sure the Dgraph upsert -// mutation ends up like: -// -// query : -// Author1 as Author1(func: eq(username, "A-user")) @filter(type(Author)) { uid } -// condition : -// len(Author1) > 0 -// mutation : -// { "uid": "XYZ", "title": "...", "author": { "id": "uid(Author1)", "posts": ... -// where asXIDReference builds the fragment -// { "id": "uid(Author1)", "posts": [ { "uid": "XYZ" } ] } -func asXIDReference( - ctx context.Context, - srcField schema.FieldDefinition, - srcUID string, - typ schema.Type, - xidFieldName, xidString, xidVariable string, - withAdditionalDeletes bool, - varGen *VariableGenerator, - xidMetadata *xidMetadata) *mutationFragment { - - result := make(map[string]interface{}, 2) - frag := newFragment(result) - - result["uid"] = fmt.Sprintf("uid(%s)", xidVariable) - - addInverseLink(result, srcField, srcUID) - - // add the query only if it has not been added already, otherwise we will be assigning same - // variable name more than once in queries, resulting in dgraph error - if !xidMetadata.queryExists[xidVariable] { - frag.queries = []*gql.GraphQuery{xidQuery(xidVariable, xidString, xidFieldName, typ)} - xidMetadata.queryExists[xidVariable] = true - } - frag.conditions = []string{fmt.Sprintf("eq(len(%s), 1)", xidVariable)} - frag.check = checkQueryResult(xidVariable, - nil, - errors.Errorf("ID \"%s\" isn't a %s", xidString, srcField.Type().Name())) - - if withAdditionalDeletes { - addAdditionalDeletes(ctx, frag, varGen, srcField, srcUID, xidVariable) - } - - return frag -} - // addAdditionalDeletes creates any additional deletes that are needed when a reference changes. // E.g. if we have // type Post { ... author: Author @hasInverse(field: posts) ... } @@ -1599,7 +1781,8 @@ func addAdditionalDeletes( ctx context.Context, frag *mutationFragment, varGen *VariableGenerator, - srcField schema.FieldDefinition, srcUID, variable string) { + srcField schema.FieldDefinition, + srcUID, variable string) { if srcField == nil { return @@ -1716,7 +1899,12 @@ func addDelete( frag.queries = append(frag.queries, qry) - del := fmt.Sprintf("uid(%s)", qryVar) + del := qryVar + // Add uid around qryVar in case qryVar is not UID. + if _, err := asUID(qryVar); err != nil { + del = fmt.Sprintf("uid(%s)", qryVar) + } + if delFld.Type().ListType() == nil { frag.deletes = append(frag.deletes, map[string]interface{}{ @@ -1849,71 +2037,6 @@ func addInverseLink(obj map[string]interface{}, srcField schema.FieldDefinition, } } -func xidQuery(xidVariable, xidString, xidPredicate string, typ schema.Type) *gql.GraphQuery { - qry := &gql.GraphQuery{ - Var: xidVariable, - Attr: xidVariable, - Func: &gql.Function{ - Name: "eq", - Args: []gql.Arg{ - {Value: typ.DgraphPredicate(xidPredicate)}, - {Value: maybeQuoteArg("eq", xidString)}, - }, - }, - Children: []*gql.GraphQuery{{Attr: "uid"}}, - } - addTypeFilter(qry, typ) - return qry -} - -func rewriteList( - ctx context.Context, - parentTyp schema.Type, - typ schema.Type, - srcField schema.FieldDefinition, - srcUID string, - varGen *VariableGenerator, - withAdditionalDeletes bool, - objects []interface{}, - deepXID int, - xidMetadata *xidMetadata) *mutationRes { - - result := &mutationRes{} - result.secondPass = []*mutationFragment{newFragment(make([]interface{}, 0))} - foundSecondPass := false - - for i, obj := range objects { - switch obj := obj.(type) { - case map[string]interface{}: - var frag *mutationRes - if typ.IsUnion() { - frag = rewriteUnionField(ctx, parentTyp, srcField, srcUID, varGen, - withAdditionalDeletes, obj, deepXID, xidMetadata, i) - } else { - frag = rewriteObject(ctx, parentTyp, typ, srcField, srcUID, varGen, - withAdditionalDeletes, obj, deepXID, xidMetadata) - } - if len(frag.secondPass) != 0 { - foundSecondPass = true - } - result.firstPass = appendFragments(result.firstPass, frag.firstPass) - result.secondPass = squashFragments(squashIntoList, result.secondPass, frag.secondPass) - default: - // All objects in the list must be of the same type. GraphQL validation makes sure - // of that. So this must be a list of scalar values (lists of lists aren't allowed). - return &mutationRes{secondPass: []*mutationFragment{ - newFragment(objects), - }} - } - } - - if len(objects) != 0 && !foundSecondPass { - result.secondPass = nil - } - - return result -} - func newFragment(f interface{}) *mutationFragment { return &mutationFragment{ fragment: f, @@ -1922,219 +2045,6 @@ func newFragment(f interface{}) *mutationFragment { } } -func squashIntoList(list, v interface{}, makeCopy bool) interface{} { - if list == nil { - return []interface{}{v} - } - asList := list.([]interface{}) - if makeCopy { - cpy := make([]interface{}, len(asList), len(asList)+1) - copy(cpy, asList) - asList = cpy - } - return append(asList, v) -} - -func squashIntoObject(label string) func(interface{}, interface{}, bool) interface{} { - return func(object, v interface{}, makeCopy bool) interface{} { - asObject := object.(map[string]interface{}) - if makeCopy { - cpy := make(map[string]interface{}, len(asObject)+1) - for k, v := range asObject { - cpy[k] = v - } - asObject = cpy - } - - 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 - } -} - -func appendFragments(left, right []*mutationFragment) []*mutationFragment { - if len(left) == 0 { - return right - } - - if len(right) == 0 { - return left - } - - result := make([]*mutationFragment, len(left)+len(right)) - i := 0 - - var queries []*gql.GraphQuery - for _, l := range left { - queries = append(queries, l.queries...) - result[i] = l - result[i].queries = []*gql.GraphQuery{} - result[i].newNodes = make(map[string]schema.Type) - i++ - } - - for _, r := range right { - queries = append(queries, r.queries...) - result[i] = r - result[i].queries = []*gql.GraphQuery{} - result[i].newNodes = make(map[string]schema.Type) - i++ - } - - newNodes := make(map[string]schema.Type) - for _, l := range left { - copyTypeMap(l.newNodes, newNodes) - } - for _, r := range right { - copyTypeMap(r.newNodes, newNodes) - } - - result[0].newNodes = newNodes - result[0].queries = queries - - return result -} - -// squashFragments takes two lists of mutationFragments and produces a single list -// that has all the right fragments squashed into the left. -// -// In most cases, this is len(left) == 1 and len(right) == 1 and the result is a -// single fragment. For example, if left is what we have built so far for adding a -// new author and to original input contained: -// { -// ... -// country: { id: "0x123" } -// } -// rewriteObject is called on `{ id: "0x123" }` to create a fragment with -// Query: CountryXYZ as CountryXYZ(func: uid(0x123)) @filter(type(Country)) { uid } -// Condition: eq(len(CountryXYZ), 1) -// Fragment: { id: "0x123" } -// In this case, we just need to add `country: { id: "0x123" }`, the query and condition -// to the left fragment and the result is a single fragment. If there are no XIDs -// in the schema, only 1 fragment can ever be generated. We can always tell if the -// mutation means to link to an existing object (because the ID value is present), -// or if the intention is to create a new object (because the ID value isn't there, -// that means it's not known client side), so there's never any need for more than -// one conditional mutation. -// -// However, if there are XIDs, there can be multiple possible mutations. -// For example, if schema has `Type Country { code: String! @id, name: String! ... }` -// and the mutation input is -// { -// ... -// country: { code: "ind", name: "India" } -// } -// we can't tell from the mutation text if this mutation means to link to an existing -// country or if it's a deep add on the XID `code: "ind"`. If the mutation was -// `country: { code: "ind" }`, we'd know it's a link because they didn't supply -// all the ! fields to correctly create a new country, but from -// `country: { code: "ind", name: "India" }` we have to go to the DB to check. -// So rewriteObject called on `{ code: "ind", name: "India" }` produces two fragments -// -// Query: CountryXYZ as CountryXYZ(func: eq(code, "ind")) @filter(type(Country)) { uid } -// -// Fragment1 (if "ind" already exists) -// Cond: eq(len(CountryXYZ), 1) -// Fragment: { uid: uid(CountryXYZ) } -// -// and -// -// Fragment2 (if "ind" doesn't exist) -// Cond eq(len(CountryXYZ), 0) -// Fragment: { uid: uid(CountryXYZ), code: "ind", name: "India" } -// -// Now we have to squash this into what we've already built for the author (left -// mutationFragment). That'll end up as a result with two fragments (two possible -// mutations guarded by conditions on if the country exists), and to do -// that, we'll need to make some copies, e.g., because we'll end up with -// country: { uid: uid(CountryXYZ) } -// in one fragment, and -// country: { uid: uid(CountryXYZ), code: "ind", name: "India" } -// in the other we need to copy what we've already built for the author to represent -// the different mutation payloads. Same goes for the conditions. -func squashFragments( - combiner func(interface{}, interface{}, bool) interface{}, - left, right []*mutationFragment) []*mutationFragment { - - if len(left) == 0 { - return right - } - - if len(right) == 0 { - return left - } - - result := make([]*mutationFragment, 0, len(left)*len(right)) - for _, l := range left { - for _, r := range right { - var conds []string - var deletes []interface{} - - if len(l.conditions) > 0 { - conds = make([]string, len(l.conditions), len(l.conditions)+len(r.conditions)) - copy(conds, l.conditions) - } - - if len(l.deletes) > 0 { - deletes = make([]interface{}, len(l.deletes), len(l.deletes)+len(r.deletes)) - copy(deletes, l.deletes) - } - - result = append(result, &mutationFragment{ - conditions: append(conds, r.conditions...), - deletes: append(deletes, r.deletes...), - fragment: combiner(l.fragment, r.fragment, len(right) > 1), - check: func(lcheck, rcheck resultChecker) resultChecker { - return func(m map[string]interface{}) error { - return schema.AppendGQLErrs(lcheck(m), rcheck(m)) - } - }(l.check, r.check), - err: schema.AppendGQLErrs(l.err, r.err), - }) - } - } - - // queries and node types don't need copying, they just need to be all collected - // at the end, so accumulate them all into one of the result fragments - var queries []*gql.GraphQuery - for _, l := range left { - queries = append(queries, l.queries...) - } - for _, r := range right { - queries = append(queries, r.queries...) - } - - newNodes := make(map[string]schema.Type) - for _, l := range left { - copyTypeMap(l.newNodes, newNodes) - } - for _, r := range right { - copyTypeMap(r.newNodes, newNodes) - } - result[0].newNodes = newNodes - result[0].queries = queries - - return result -} - func copyTypeMap(from, to map[string]schema.Type) { for name, typ := range from { to[name] = typ diff --git a/graphql/resolve/mutation_test.go b/graphql/resolve/mutation_test.go index 25aa27c6ef7..bdde99bf86c 100644 --- a/graphql/resolve/mutation_test.go +++ b/graphql/resolve/mutation_test.go @@ -53,7 +53,9 @@ type testCase struct { DGQuery string DGQuerySec string Error *x.GqlError + Error2 *x.GqlError ValidationError *x.GqlError + QNameToUID string } type dgraphMutation struct { @@ -73,7 +75,7 @@ func TestMutationRewriting(t *testing.T) { mutationRewriting(t, "update_mutation_test.yaml", NewUpdateRewriter) }) t.Run("Delete Mutation Rewriting", func(t *testing.T) { - mutationRewriting(t, "delete_mutation_test.yaml", NewDeleteRewriter) + deleteMutationRewriting(t, "delete_mutation_test.yaml", NewDeleteRewriter) }) } @@ -146,8 +148,11 @@ func benchmark3LevelDeep(num int, b *testing.B) { }) mut := test.GetMutation(t, op) + addRewriter := NewAddRewriter() + idExistence := make(map[string]string) for n := 0; n < b.N; n++ { - NewAddRewriter().Rewrite(context.Background(), mut) + addRewriter.RewriteQueries(context.Background(), mut) + addRewriter.Rewrite(context.Background(), mut, idExistence) } } @@ -157,7 +162,7 @@ func Benchmark3LevelDeep100(b *testing.B) { benchmark3LevelDeep(100, b) } func Benchmark3LevelDeep1000(b *testing.B) { benchmark3LevelDeep(1000, b) } func Benchmark3LevelDeep10000(b *testing.B) { benchmark3LevelDeep(10000, b) } -func mutationRewriting(t *testing.T, file string, rewriterFactory func() MutationRewriter) { +func deleteMutationRewriting(t *testing.T, file string, rewriterFactory func() MutationRewriter) { b, err := ioutil.ReadFile(file) require.NoError(t, err, "Unable to read test file") @@ -205,7 +210,9 @@ func mutationRewriting(t *testing.T, file string, rewriterFactory func() Mutatio rewriterToTest := rewriterFactory() // -- Act -- - upsert, err := rewriterToTest.Rewrite(context.Background(), mut) + _, _ = rewriterToTest.RewriteQueries(context.Background(), mut) + idExistence := make(map[string]string) + upsert, err := rewriterToTest.Rewrite(context.Background(), mut, idExistence) // -- Assert -- if tcase.Error != nil || err != nil { require.NotNil(t, err) @@ -225,17 +232,107 @@ func mutationRewriting(t *testing.T, file string, rewriterFactory func() Mutatio } } +func mutationRewriting(t *testing.T, file string, rewriterFactory func() MutationRewriter) { + b, err := ioutil.ReadFile(file) + require.NoError(t, err, "Unable to read test file") + + var tests []testCase + err = yaml.Unmarshal(b, &tests) + require.NoError(t, err, "Unable to unmarshal tests to yaml.") + + gqlSchema := test.LoadSchemaFromFile(t, "schema.graphql") + + compareMutations := func(t *testing.T, test []*dgraphMutation, generated []*dgoapi.Mutation) { + require.Len(t, generated, len(test)) + for i, expected := range test { + require.Equal(t, expected.Cond, generated[i].Cond) + if len(generated[i].SetJson) > 0 || expected.SetJSON != "" { + require.JSONEq(t, expected.SetJSON, string(generated[i].SetJson)) + } + + if len(generated[i].DeleteJson) > 0 || expected.DeleteJSON != "" { + require.JSONEq(t, expected.DeleteJSON, string(generated[i].DeleteJson)) + } + } + } + + for _, tcase := range tests { + t.Run(tcase.Name, func(t *testing.T) { + // -- Arrange -- + var vars map[string]interface{} + if tcase.GQLVariables != "" { + err := json.Unmarshal([]byte(tcase.GQLVariables), &vars) + require.NoError(t, err) + } + + op, err := gqlSchema.Operation( + &schema.Request{ + Query: tcase.GQLMutation, + Variables: vars, + }) + if tcase.ValidationError != nil { + require.NotNil(t, err) + require.Equal(t, tcase.ValidationError.Error(), err.Error()) + return + } else { + require.NoError(t, err) + } + mut := test.GetMutation(t, op) + + rewriterToTest := rewriterFactory() + + // -- Query -- + queries, err := rewriterToTest.RewriteQueries(context.Background(), mut) + // -- Assert -- + if tcase.Error != nil || err != nil { + require.NotNil(t, err) + require.NotNil(t, tcase.Error) + require.Equal(t, tcase.Error.Error(), err.Error()) + return + } + require.Equal(t, tcase.DGQuery, dgraph.AsString(queries)) + + // -- Parse qNameToUID map + qNameToUID := make(map[string]string) + if tcase.QNameToUID != "" { + err = json.Unmarshal([]byte(tcase.QNameToUID), &qNameToUID) + require.NoError(t, err) + } + + // Mutate + upsert, err := rewriterToTest.Rewrite(context.Background(), mut, qNameToUID) + if tcase.Error2 != nil || err != nil { + require.NotNil(t, err) + require.NotNil(t, tcase.Error2) + require.Equal(t, tcase.Error2.Error(), err.Error()) + return + } + require.NoError(t, err) + require.Equal(t, 1, len(upsert)) + compareMutations(t, tcase.DGMutations, upsert[0].Mutations) + + // Compare the query generated along with mutations. + dgQuerySec := dgraph.AsString(upsert[0].Query) + require.Equal(t, tcase.DGQuerySec, dgQuerySec) + }) + } +} + func TestMutationQueryRewriting(t *testing.T) { testTypes := map[string]struct { - mut string - rewriter func() MutationRewriter - assigned map[string]string - result map[string]interface{} + mut string + payloadType string + rewriter func() MutationRewriter + idExistence map[string]string + assigned map[string]string + result map[string]interface{} }{ "Add Post ": { - mut: `addPost(input: [{title: "A Post", author: {id: "0x1"}}])`, - rewriter: NewAddRewriter, - assigned: map[string]string{"Post1": "0x4"}, + mut: `addPost(input: [{title: "A Post", author: {id: "0x1"}}])`, + payloadType: "AddPostPayload", + rewriter: NewAddRewriter, + idExistence: map[string]string{"Author1": "0x1"}, + assigned: map[string]string{"Post2": "0x4"}, }, "Update Post ": { mut: `updatePost(input: {filter: {postID @@ -275,7 +372,9 @@ func TestMutationQueryRewriting(t *testing.T) { }) require.NoError(t, err) gqlMutation := test.GetMutation(t, op) - _, err = rewriter.Rewrite(context.Background(), gqlMutation) + + _, _ = rewriter.RewriteQueries(context.Background(), gqlMutation) + _, err = rewriter.Rewrite(context.Background(), gqlMutation, tt.idExistence) require.Nil(t, err) // -- Act -- diff --git a/graphql/resolve/resolver_error_test.go b/graphql/resolve/resolver_error_test.go index 3857f54a873..11381cf3bd7 100644 --- a/graphql/resolve/resolver_error_test.go +++ b/graphql/resolve/resolver_error_test.go @@ -42,9 +42,14 @@ import ( // to see what the test is actually doing. type executor struct { - resp string - assigned map[string]string - result map[string]interface{} + // existenceQueriesResp stores JSON response of the existence queries in case of Add + // or Update mutations and is returned for every third Execute call. + // counter is used to count how many times Execute function has been called. + existenceQueriesResp string + counter int + resp string + assigned map[string]string + result map[string]interface{} queryTouched uint64 mutationTouched uint64 @@ -83,6 +88,15 @@ type Post { }` func (ex *executor) Execute(ctx context.Context, req *dgoapi.Request) (*dgoapi.Response, error) { + // In case ex.existenceQueriesResp is non empty, its an Add or an Update mutation. In this case, + // every third call to Execute + // query is an existence query and existenceQueriesResp is returned. + ex.counter++ + if ex.existenceQueriesResp != "" && ex.counter%3 == 1 { + return &dgoapi.Response{ + Json: []byte(ex.existenceQueriesResp), + }, nil + } if len(req.Mutations) == 0 { ex.failQuery-- if ex.failQuery == 0 { @@ -215,9 +229,10 @@ func TestAddMutationUsesErrorPropagation(t *testing.T) { t.Run(name, func(t *testing.T) { resp := resolveWithClient(gqlSchema, mutation, nil, &executor{ - resp: tcase.queryResponse, - assigned: tcase.mutResponse, - result: tcase.mutQryResp, + existenceQueriesResp: `{ "Author1": [{"uid":"0x1"}]}`, + resp: tcase.queryResponse, + assigned: tcase.mutResponse, + result: tcase.mutQryResp, }) test.RequireJSONEq(t, tcase.errors, resp.Errors) @@ -310,7 +325,6 @@ func TestUpdateMutationUsesErrorPropagation(t *testing.T) { // So this mocks a failing mutation and tests that we behave correctly in the case // of multiple mutations. func TestManyMutationsWithError(t *testing.T) { - // add1 - should succeed // add2 - should fail // add3 - is never executed @@ -387,9 +401,10 @@ func TestManyMutationsWithError(t *testing.T) { multiMutation, map[string]interface{}{"id": tcase.idValue}, &executor{ - resp: tcase.queryResponse, - assigned: tcase.mutResponse, - failMutation: 2}) + existenceQueriesResp: `{ "Author1": [{"uid":"0x1"}]}`, + resp: tcase.queryResponse, + assigned: tcase.mutResponse, + failMutation: 2}) if diff := cmp.Diff(tcase.errors, resp.Errors); diff != "" { t.Errorf("errors mismatch (-want +got):\n%s", diff) diff --git a/graphql/resolve/resolver_test.go b/graphql/resolve/resolver_test.go index 9b2e5412a89..ca58dc7e89b 100644 --- a/graphql/resolve/resolver_test.go +++ b/graphql/resolve/resolver_test.go @@ -351,7 +351,6 @@ func TestQueryAlias(t *testing.T) { } func TestMutationAlias(t *testing.T) { - tests := map[string]struct { gqlQuery string mutResponse map[string]string @@ -408,9 +407,10 @@ func TestMutationAlias(t *testing.T) { t.Run(name, func(t *testing.T) { resp := resolveWithClient(gqlSchema, tcase.gqlQuery, nil, &executor{ - resp: tcase.queryResponse, - assigned: tcase.mutResponse, - result: tcase.mutQryResp, + existenceQueriesResp: `{ "Author1": [{"uid":"0x1"}]}`, + resp: tcase.queryResponse, + assigned: tcase.mutResponse, + result: tcase.mutQryResp, }) require.Nil(t, resp.Errors) diff --git a/graphql/resolve/update_mutation_test.yaml b/graphql/resolve/update_mutation_test.yaml index b700cb4ca13..d968441f1fd 100644 --- a/graphql/resolve/update_mutation_test.yaml +++ b/graphql/resolve/update_mutation_test.yaml @@ -23,6 +23,12 @@ } } explanation: "The update patch should get rewritten into the Dgraph set mutation" + dgquerysec: |- + query { + x as updateHotel(func: type(Hotel)) @filter(near(Hotel.location, [22.22,11.11], 33.33)) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", @@ -32,12 +38,6 @@ } } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updateHotel(func: type(Hotel)) @filter(near(Hotel.location, [22.22,11.11], 33.33)) { - uid - } - } - name: "Update remove mutation on Geo - Point type" @@ -64,6 +64,12 @@ } } explanation: "The update patch should get rewritten into the Dgraph delete mutation" + dgquerysec: |- + query { + x as updateHotel(func: uid(0x123, 0x124)) @filter(type(Hotel)) { + uid + } + } dgmutations: - deletejson: | { "uid" : "uid(x)", @@ -73,12 +79,7 @@ } } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updateHotel(func: uid(0x123, 0x124)) @filter(type(Hotel)) { - uid - } - } + - name: "Update remove mutation on Geo - Polygon type" @@ -133,6 +134,12 @@ } } explanation: "The update patch should get rewritten into the Dgraph delete mutation" + dgquerysec: |- + query { + x as updateHotel(func: uid(0x123, 0x124)) @filter(type(Hotel)) { + uid + } + } dgmutations: - deletejson: | { "uid" : "uid(x)", @@ -142,12 +149,6 @@ } } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updateHotel(func: uid(0x123, 0x124)) @filter(type(Hotel)) { - uid - } - } - name: "Update set mutation on Geo - MultiPolygon type" @@ -218,6 +219,12 @@ } } explanation: "The update patch should get rewritten into the Dgraph set mutation" + dgquerysec: |- + query { + x as updateHotel(func: uid(0x123, 0x124)) @filter(type(Hotel)) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", @@ -227,12 +234,6 @@ } } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updateHotel(func: uid(0x123, 0x124)) @filter(type(Hotel)) { - uid - } - } - name: "Update set mutation with variables" @@ -255,18 +256,18 @@ } } explanation: "The update patch should get rewritten into the Dgraph set mutation" + dgquerysec: |- + query { + x as updatePost(func: uid(0x123, 0x124)) @filter(type(Post)) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", "Post.text": "updated text" } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updatePost(func: uid(0x123, 0x124)) @filter(type(Post)) { - uid - } - } - name: "Update remove mutation with variables and value" @@ -289,18 +290,18 @@ } } explanation: "The update patch should get rewritten into the Dgraph delete mutation" + dgquerysec: |- + query { + x as updatePost(func: uid(0x123, 0x124)) @filter(type(Post)) { + uid + } + } dgmutations: - deletejson: | { "uid" : "uid(x)", "Post.text": "delete this text" } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updatePost(func: uid(0x123, 0x124)) @filter(type(Post)) { - uid - } - } - name: "Update delete mutation with variables and null" @@ -323,18 +324,18 @@ } } explanation: "The update patch should get rewritten into the Dgraph mutation" + dgquerysec: |- + query { + x as updatePost(func: uid(0x123, 0x124)) @filter(type(Post)) { + uid + } + } dgmutations: - deletejson: | { "uid" : "uid(x)", "Post.text": null } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updatePost(func: uid(0x123, 0x124)) @filter(type(Post)) { - uid - } - } - name: "Update mutation for a type that implements an interface" @@ -362,6 +363,12 @@ } } explanation: "The mutation should get rewritten with correct edges from the interface." + dgquerysec: |- + query { + x as updateHuman(func: uid(0x123)) @filter(type(Human)) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", @@ -371,12 +378,6 @@ "Human.female": true } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updateHuman(func: uid(0x123)) @filter(type(Human)) { - uid - } - } - name: "Update mutation for an interface" @@ -390,18 +391,18 @@ } } explanation: "The mutation should get rewritten with correct edges from the interface." + dgquerysec: |- + query { + x as updateCharacter(func: uid(0x123)) @filter(type(Character)) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", "Character.name": "Bob" } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updateCharacter(func: uid(0x123)) @filter(type(Character)) { - uid - } - } - name: "Update mutation using filters" @@ -424,18 +425,18 @@ } } explanation: "The update patch should get rewritten into the Dgraph mutation" + dgquerysec: |- + query { + x as updatePost(func: type(Post)) @filter(eq(Post.tags, "foo")) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", "Post.text": "updated text" } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updatePost(func: type(Post)) @filter(eq(Post.tags, "foo")) { - uid - } - } - name: "Update mutation using code" @@ -458,18 +459,19 @@ } } explanation: "The update mutation should get rewritten into a Dgraph upsert mutation" + dgquerysec: |- + query { + x as updateState(func: type(State)) @filter(eq(State.code, "nsw")) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", "State.name": "nsw" } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updateState(func: type(State)) @filter(eq(State.code, "nsw")) { - uid - } - } + - name: "Update mutation using code on type which also has an ID field" @@ -493,18 +495,19 @@ } } explanation: "The update mutation should get rewritten into a Dgraph upsert mutation" + dgquerysec: |- + query { + x as updateEditor(func: uid(0x1, 0x2)) @filter((eq(Editor.code, "editor") AND type(Editor))) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", "Editor.name": "A.N. Editor" } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updateEditor(func: uid(0x1, 0x2)) @filter((eq(Editor.code, "editor") AND type(Editor))) { - uid - } - } + - name: "Update add reference" @@ -526,6 +529,25 @@ } } } + dgquery: |- + query { + Post1(func: uid(0x456)) @filter(type(Post)) { + uid + } + } + qnametouid: | + { + "Post1": "0x456" + } + dgquerysec: |- + query { + x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { + uid + } + var(func: uid(0x456)) { + Author4 as Post.author @filter(NOT (uid(x))) + } + } dgmutations: - setjson: | { "uid" : "uid(x)", @@ -540,22 +562,10 @@ [ { "uid": "uid(Author4)", - "Author.posts": [{"uid": "uid(Post3)"}] + "Author.posts": [{"uid": "0x456"}] } ] - cond: "@if(eq(len(Post3), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { - uid - } - Post3 as Post3(func: uid(0x456)) @filter(type(Post)) { - uid - } - var(func: uid(Post3)) { - Author4 as Post.author @filter(NOT (uid(x))) - } - } + cond: "@if(gt(len(x), 0))" - name: "Update remove without XID or ID" @@ -580,9 +590,9 @@ } } explanation: "Remove requires an XID or ID" - error: + error2: { "message": - "failed to rewrite mutation payload because name is not provided" } + "failed to rewrite mutation payload because field name cannot be empty" } - name: "Update remove with XID" @@ -607,28 +617,34 @@ } } } + dgquery: |- + query { + ComputerOwner1(func: eq(ComputerOwner.name, "computerOwnerName")) @filter(type(ComputerOwner)) { + uid + } + } + qnametouid: | + { + "ComputerOwner1": "0x123" + } + dgquerysec: |- + query { + x as updateComputer(func: type(Computer)) @filter(eq(Computer.name, "computerName")) { + uid + } + } dgmutations: - deletejson: | { "Computer.owners": [{ - "uid" : "uid(ComputerOwner4)", + "uid" : "0x123", "ComputerOwner.computers": { "uid": "uid(x)" } }], "uid" : "uid(x)" } - cond: "@if(eq(len(ComputerOwner4), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateComputer(func: type(Computer)) @filter(eq(Computer.name, "computerName")) { - uid - } - ComputerOwner4 as ComputerOwner4(func: eq(ComputerOwner.name, "computerOwnerName")) @filter(type(ComputerOwner)) { - uid - } - } - + cond: "@if(gt(len(x), 0))" - name: "Update remove with ID" @@ -650,6 +666,22 @@ } } } + dgquery: |- + query { + Post1(func: uid(0x124)) @filter(type(Post)) { + uid + } + } + qnametouid: | + { + "Post1": "0x124" + } + dgquerysec: |- + query { + x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { + uid + } + } dgmutations: - deletejson: | { @@ -661,17 +693,7 @@ }], "uid" : "uid(x)" } - cond: "@if(eq(len(Post3), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { - uid - } - Post3 as Post3(func: uid(0x124)) @filter(type(Post)) { - uid - } - } - + cond: "@if(gt(len(x), 0))" - name: "Update remove reference" @@ -693,6 +715,22 @@ } } } + dgquery: |- + query { + Post1(func: uid(0x456)) @filter(type(Post)) { + uid + } + } + qnametouid: | + { + "Post1": "0x456" + } + dgquerysec: |- + query { + x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { + uid + } + } dgmutations: - deletejson: | { "uid" : "uid(x)", @@ -703,16 +741,7 @@ } ] } - cond: "@if(eq(len(Post3), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { - uid - } - Post3 as Post3(func: uid(0x456)) @filter(type(Post)) { - uid - } - } + cond: "@if(gt(len(x), 0))" - name: "Update remove reference without id or xid" @@ -737,9 +766,9 @@ } } } - error: + error2: message: |- - failed to rewrite mutation payload because object of type: Node doesn't have a field of type ID! or @id and can't be referenced for deletion + failed to rewrite mutation payload because id is not provided - name: "Update add and remove together" @@ -764,24 +793,47 @@ } } } - dgmutations: - - setjson: | - { "uid" : "uid(x)", - "Author.posts": [ - { - "uid": "0x456", - "Post.author": { "uid": "uid(x)" } - } - ] - } - deletejson: | - [ - { - "uid": "uid(Author4)", - "Author.posts": [{"uid": "uid(Post3)"}] + dgquery: |- + query { + Post1(func: uid(0x456)) @filter(type(Post)) { + uid + } + Post2(func: uid(0x789)) @filter(type(Post)) { + uid + } + } + qnametouid: | + { + "Post1": "0x456", + "Post2": "0x789" + } + dgquerysec: |- + query { + x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { + uid + } + var(func: uid(0x456)) { + Author5 as Post.author @filter(NOT (uid(x))) + } + } + dgmutations: + - setjson: | + { "uid" : "uid(x)", + "Author.posts": [ + { + "uid": "0x456", + "Post.author": { "uid": "uid(x)" } + } + ] + } + deletejson: | + [ + { + "uid": "uid(Author5)", + "Author.posts": [{"uid": "0x456"}] } ] - cond: "@if(eq(len(Post3), 1) AND gt(len(x), 0))" + cond: "@if(gt(len(x), 0))" - deletejson: | { "uid" : "uid(x)", "Author.posts": [ @@ -791,22 +843,7 @@ } ] } - cond: "@if(eq(len(Post6), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { - uid - } - Post3 as Post3(func: uid(0x456)) @filter(type(Post)) { - uid - } - var(func: uid(Post3)) { - Author4 as Post.author @filter(NOT (uid(x))) - } - Post6 as Post6(func: uid(0x789)) @filter(type(Post)) { - uid - } - } + cond: "@if(gt(len(x), 0))" - name: "Deep updates don't alter linked objects" @@ -833,6 +870,25 @@ } } explanation: "updateAuthor doesn't update posts except where references are removed" + dgquery: |- + query { + Post1(func: uid(0x456)) @filter(type(Post)) { + uid + } + } + qnametouid: | + { + "Post1": "0x456" + } + dgquerysec: |- + query { + x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { + uid + } + var(func: uid(0x456)) { + Author4 as Post.author @filter(NOT (uid(x))) + } + } dgmutations: - setjson: | { "uid" : "uid(x)", @@ -847,22 +903,10 @@ [ { "uid": "uid(Author4)", - "Author.posts": [{"uid": "uid(Post3)"}] + "Author.posts": [{"uid": "0x456"}] } ] - cond: "@if(eq(len(Post3), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { - uid - } - Post3 as Post3(func: uid(0x456)) @filter(type(Post)) { - uid - } - var(func: uid(Post3)) { - Author4 as Post.author @filter(NOT (uid(x))) - } - } + cond: "@if(gt(len(x), 0))" - name: "Deep update" @@ -887,6 +931,12 @@ } } explanation: "The update creates a new country" + dgquerysec: |- + query { + x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", @@ -897,15 +947,9 @@ } } cond: "@if(gt(len(x), 0))" - dgquery: |- - query { - x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { - uid - } - } - - name: "Deep xid create options" + name: "Deep xid create options 1" gqlmutation: | mutation updateAuthor($patch: UpdateAuthorInput!) { updateAuthor(input: $patch) { @@ -930,62 +974,111 @@ } } } - explanation: "The update has a choice of linking to new or existing state" + explanation: "The update creates a new state" + dgquery: |- + query { + State1(func: eq(State.code, "dg")) @filter(type(State)) { + uid + } + } + dgquerysec: |- + query { + x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { + uid + } + } dgmutations: - - setjson: | - { - "State.code": "dg", - "State.name": "Dgraph", - "dgraph.type": [ - "State" - ], - "uid": "_:State5" - } - cond: "@if(eq(len(State5), 0) AND gt(len(x), 0))" - dgmutationssec: - setjson: | { "uid" : "uid(x)", "Author.country": { - "uid": "_:Country3", + "uid": "_:Country4", "dgraph.type": ["Country"], "Country.name": "New Country", "Country.states": [ { - "uid": "uid(State5)", + "State.code": "dg", + "State.name": "Dgraph", + "dgraph.type": [ + "State" + ], + "uid": "_:State1", "State.country": { - "uid": "_:Country3" + "uid": "_:Country4" } } ] } } - deletejson: | - [ - { - "uid": "uid(Country6)", - "Country.states": [{"uid": "uid(State5)"}] + cond: "@if(gt(len(x), 0))" + +- + name: "Deep xid create options 2" + gqlmutation: | + mutation updateAuthor($patch: UpdateAuthorInput!) { + updateAuthor(input: $patch) { + author { + id + } + } + } + gqlvariables: | + { "patch": + { "filter": { + "id": ["0x123"] + }, + "set": { + "country": { + "name": "New Country", + "states": [ { + "code": "dg", + "name": "Dgraph" + } ] } - ] - cond: "@if(eq(len(State5), 1) AND gt(len(x), 0))" + } + } + } + explanation: "The update links to existing state" dgquery: |- query { - x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { - uid - } - State5 as State5(func: eq(State.code, "dg")) @filter(type(State)) { + State1(func: eq(State.code, "dg")) @filter(type(State)) { uid } } + qnametouid: | + { + "State1": "0x987" + } dgquerysec: |- query { x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { uid } - State5 as State5(func: eq(State.code, "dg")) @filter(type(State)) { - uid - } - var(func: uid(State5)) { - Country6 as State.country + var(func: uid(0x987)) { + Country5 as State.country } } + dgmutations: + - setjson: | + { "uid" : "uid(x)", + "Author.country": { + "uid": "_:Country4", + "dgraph.type": ["Country"], + "Country.name": "New Country", + "Country.states": [ { + "uid": "0x987", + "State.country": { + "uid": "_:Country4" + } + } ] + } + } + deletejson: | + [ + { + "uid": "uid(Country5)", + "Country.states": [{"uid": "0x987"}] + } + ] + cond: "@if(gt(len(x), 0))" + - name: "Deep xid link only" @@ -1013,17 +1106,36 @@ } } explanation: "The update must link to the existing state" + dgquery: |- + query { + State1(func: eq(State.code, "dg")) @filter(type(State)) { + uid + } + } + qnametouid: | + { + "State1": "0x234" + } + dgquerysec: |- + query { + x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { + uid + } + var(func: uid(0x234)) { + Country5 as State.country + } + } dgmutations: - setjson: | { "uid" : "uid(x)", "Author.country": { - "uid": "_:Country3", + "uid": "_:Country4", "dgraph.type": ["Country"], "Country.name": "New Country", "Country.states": [ { - "uid": "uid(State5)", + "uid": "0x234", "State.country": { - "uid": "_:Country3" + "uid": "_:Country4" } } ] } @@ -1031,23 +1143,11 @@ deletejson: | [ { - "uid": "uid(Country6)", - "Country.states": [{"uid": "uid(State5)"}] + "uid": "uid(Country5)", + "Country.states": [{"uid": "0x234"}] } ] - cond: "@if(eq(len(State5), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { - uid - } - State5 as State5(func: eq(State.code, "dg")) @filter(type(State)) { - uid - } - var(func: uid(State5)) { - Country6 as State.country - } - } + cond: "@if(gt(len(x), 0))" - name: "update two single edges" @@ -1072,6 +1172,28 @@ } } explanation: " Owner 0x123" + dgquery: |- + query { + House1(func: uid(0x456)) @filter(type(House)) { + uid + } + } + qnametouid: | + { + "House1": "0x456" + } + dgquerysec: |- + query { + x as updateOwner(func: uid(0x123)) @filter(type(Owner)) { + uid + } + var(func: uid(0x456)) { + Owner4 as House.owner @filter(NOT (uid(x))) + } + var(func: uid(x)) { + House5 as Owner.house @filter(NOT (uid(0x456))) + } + } dgmutations: - setjson: | { "uid" : "uid(x)", @@ -1084,29 +1206,14 @@ [ { "uid": "uid(Owner4)", - "Owner.house": {"uid": "uid(House3)"} + "Owner.house": {"uid": "0x456"} }, { "uid": "uid(House5)", "House.owner": {"uid": "uid(x)"} } ] - cond: "@if(eq(len(House3), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateOwner(func: uid(0x123)) @filter(type(Owner)) { - uid - } - House3 as House3(func: uid(0x456)) @filter(type(House)) { - uid - } - var(func: uid(House3)) { - Owner4 as House.owner @filter(NOT (uid(x))) - } - var(func: uid(x)) { - House5 as Owner.house @filter(NOT (uid(House3))) - } - } + cond: "@if(gt(len(x), 0))" - name: "Update add reference doesn't add reverse edge" @@ -1128,6 +1235,22 @@ } } } + dgquery: |- + query { + Movie1(func: uid(0x456)) @filter(type(Movie)) { + uid + } + } + qnametouid: | + { + "Movie1": "0x456" + } + dgquerysec: |- + query { + x as updateMovieDirector(func: uid(0x123)) @filter(type(MovieDirector)) { + uid + } + } dgmutations: - setjson: | { "uid" : "uid(x)", @@ -1137,16 +1260,7 @@ } ] } - cond: "@if(eq(len(Movie3), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateMovieDirector(func: uid(0x123)) @filter(type(MovieDirector)) { - uid - } - Movie3 as Movie3(func: uid(0x456)) @filter(type(Movie)) { - uid - } - } + cond: "@if(gt(len(x), 0))" - name: "Update remove reference doesn't try to remove reverse edge." @@ -1168,6 +1282,22 @@ } } } + dgquery: |- + query { + Movie1(func: uid(0x456)) @filter(type(Movie)) { + uid + } + } + qnametouid: | + { + "Movie1": "0x456" + } + dgquerysec: |- + query { + x as updateMovieDirector(func: uid(0x123)) @filter(type(MovieDirector)) { + uid + } + } dgmutations: - deletejson: | { "uid" : "uid(x)", @@ -1177,16 +1307,7 @@ } ] } - cond: "@if(eq(len(Movie3), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateMovieDirector(func: uid(0x123)) @filter(type(MovieDirector)) { - uid - } - Movie3 as Movie3(func: uid(0x456)) @filter(type(Movie)) { - uid - } - } + cond: "@if(gt(len(x), 0))" - name: "Deep Mutation Duplicate XIDs with same object Test" gqlmutation: | @@ -1221,10 +1342,7 @@ is same, it should not return error." dgquery: |- query { - x as updateStudent(func: uid(0x123)) @filter(type(Student)) { - uid - } - Teacher4 as Teacher4(func: eq(People.xid, "T1")) @filter(type(Teacher)) { + Teacher1(func: eq(People.xid, "T1")) @filter(type(Teacher)) { uid } } @@ -1233,35 +1351,26 @@ x as updateStudent(func: uid(0x123)) @filter(type(Student)) { uid } - Teacher4 as Teacher4(func: eq(People.xid, "T1")) @filter(type(Teacher)) { - uid - } } dgmutations: - - setjson: | - { - "People.name": "Teacher1", - "People.xid": "T1", - "dgraph.type": [ - "Teacher", - "People" - ], - "uid": "_:Teacher4" - } - cond: "@if(eq(len(Teacher4), 0) AND gt(len(x), 0))" - dgmutationssec: - setjson: | { "Student.taughtBy":[{ - "Teacher.teaches":[{"uid":"uid(x)"}], - "uid":"uid(Teacher4)" + "Teacher.teaches":[{"uid":"uid(x)"}], + "People.name": "Teacher1", + "People.xid": "T1", + "dgraph.type": [ + "Teacher", + "People" + ], + "uid": "_:Teacher1" },{ - "Teacher.teaches":[{"uid":"uid(x)"}], - "uid":"uid(Teacher4)" + "Teacher.teaches":[{"uid":"uid(x)"}], + "uid":"_:Teacher1" }], "uid": "uid(x)" } - cond: "@if(eq(len(Teacher4), 1) AND eq(len(Teacher4), 1) AND gt(len(x), 0))" + cond: "@if(gt(len(x), 0))" - name: "Deep Mutation Duplicate XIDs with same object with @hasInverse Test" gqlmutation: | @@ -1415,7 +1524,7 @@ # edge is in the updated node, not the reference node) # * as per case two, but with the singular edge in the updated node. -- name: "Additional Deletes - Update references existing node by ID (updt list edge)" +- name: "Additional Deletes - Update references existing node by ID (update list edge)" gqlmutation: | mutation updateAuthor($patch: UpdateAuthorInput!) { updateAuthor(input: $patch) { @@ -1435,6 +1544,25 @@ } } } + dgquery: |- + query { + Post1(func: uid(0x456)) @filter(type(Post)) { + uid + } + } + qnametouid: | + { + "Post1": "0x456" + } + dgquerysec: |- + query { + x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { + uid + } + var(func: uid(0x456)) { + Author4 as Post.author @filter(NOT (uid(x))) + } + } dgmutations: - setjson: | { @@ -1450,24 +1578,12 @@ [ { "uid": "uid(Author4)", - "Author.posts": [{"uid": "uid(Post3)"}] + "Author.posts": [{"uid": "0x456"}] } ] - cond: "@if(eq(len(Post3), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updateAuthor(func: uid(0x123)) @filter(type(Author)) { - uid - } - Post3 as Post3(func: uid(0x456)) @filter(type(Post)) { - uid - } - var(func: uid(Post3)) { - Author4 as Post.author @filter(NOT (uid(x))) - } - } + cond: "@if(gt(len(x), 0))" -- name: "Additional Deletes - Update references existing node by ID (updt single edge)" +- name: "Additional Deletes - Update references existing node by ID (update single edge)" gqlmutation: | mutation updatePost($patch: UpdatePostInput!) { updatePost(input: $patch) { @@ -1487,6 +1603,25 @@ } } } + dgquery: |- + query { + Author1(func: uid(0x456)) @filter(type(Author)) { + uid + } + } + qnametouid: | + { + "Author1": "0x456" + } + dgquerysec: |- + query { + x as updatePost(func: uid(0x123)) @filter(type(Post)) { + uid + } + var(func: uid(x)) { + Author4 as Post.author @filter(NOT (uid(0x456))) + } + } dgmutations: - setjson: | { "uid" : "uid(x)", @@ -1503,21 +1638,9 @@ "Author.posts": [ { "uid": "uid(x)" } ] } ] - cond: "@if(eq(len(Author3), 1) AND gt(len(x), 0))" - dgquery: |- - query { - x as updatePost(func: uid(0x123)) @filter(type(Post)) { - uid - } - Author3 as Author3(func: uid(0x456)) @filter(type(Author)) { - uid - } - var(func: uid(x)) { - Author4 as Post.author @filter(NOT (uid(Author3))) - } - } + cond: "@if(gt(len(x), 0))" -- name: "Additional Deletes - Update references existing node by XID (updt list edge)" +- name: "Additional Deletes - Update references existing node by XID (update list edge)" gqlmutation: | mutation updateCountry($patch: UpdateCountryInput!) { updateCountry(input: $patch) { @@ -1537,40 +1660,9 @@ } } } - dgmutations: - - setjson: | - { - "uid": "_:State4", - "dgraph.type": ["State"], - "State.code": "abc", - "State.name": "Alphabet" - } - cond: "@if(eq(len(State4), 0) AND gt(len(x), 0))" - dgmutationssec: - - setjson: | - { - "uid" : "uid(x)", - "Country.states": [ - { - "uid": "uid(State4)", - "State.country": { "uid": "uid(x)" } - } - ] - } - deletejson: | - [ - { - "uid": "uid(Country5)", - "Country.states": [ { "uid": "uid(State4)" } ] - } - ] - cond: "@if(eq(len(State4), 1) AND gt(len(x), 0))" dgquery: |- query { - x as updateCountry(func: uid(0x123)) @filter(type(Country)) { - uid - } - State4 as State4(func: eq(State.code, "abc")) @filter(type(State)) { + State1(func: eq(State.code, "abc")) @filter(type(State)) { uid } } @@ -1579,13 +1671,22 @@ x as updateCountry(func: uid(0x123)) @filter(type(Country)) { uid } - State4 as State4(func: eq(State.code, "abc")) @filter(type(State)) { - uid - } - var(func: uid(State4)) { - Country5 as State.country @filter(NOT (uid(x))) - } } + dgmutations: + - setjson: | + { + "uid" : "uid(x)", + "Country.states": [ + { + "uid": "_:State1", + "dgraph.type": ["State"], + "State.code": "abc", + "State.name": "Alphabet", + "State.country": { "uid": "uid(x)" } + } + ] + } + cond: "@if(gt(len(x), 0))" - name: "Update mutation error on @id field for empty value" gqlmutation: | @@ -1611,7 +1712,7 @@ error: { "message": "failed to rewrite mutation payload because encountered an empty value for @id field `State.code`" } -- name: "Additional Deletes - Update references existing node by XID (updt single edge)" +- name: "Additional Deletes - Update references existing node by XID (update single edge)" gqlmutation: | mutation updateComputerOwner($patch: UpdateComputerOwnerInput!) { updateComputerOwner(input: $patch) { @@ -1628,36 +1729,9 @@ "set": { "computers": { "name": "Comp" } } } } - dgmutations: - - setjson: | - { - "uid": "_:Computer4", - "dgraph.type": ["Computer"], - "Computer.name": "Comp" - } - cond: "@if(eq(len(Computer4), 0) AND gt(len(x), 0))" - dgmutationssec: - - setjson: | - { "uid" : "uid(x)", - "ComputerOwner.computers": { - "uid": "uid(Computer4)", - "Computer.owners": [ { "uid": "uid(x)" } ] - } - } - deletejson: | - [ - { - "uid": "uid(Computer5)", - "Computer.owners": [ { "uid": "uid(x)" } ] - } - ] - cond: "@if(eq(len(Computer4), 1) AND gt(len(x), 0))" dgquery: |- query { - x as updateComputerOwner(func: type(ComputerOwner)) @filter(eq(ComputerOwner.name, "A.N. Owner")) { - uid - } - Computer4 as Computer4(func: eq(Computer.name, "Comp")) @filter(type(Computer)) { + Computer1(func: eq(Computer.name, "Comp")) @filter(type(Computer)) { uid } } @@ -1666,13 +1740,18 @@ x as updateComputerOwner(func: type(ComputerOwner)) @filter(eq(ComputerOwner.name, "A.N. Owner")) { uid } - Computer4 as Computer4(func: eq(Computer.name, "Comp")) @filter(type(Computer)) { - uid - } - var(func: uid(x)) { - Computer5 as ComputerOwner.computers @filter(NOT (uid(Computer4))) - } } + dgmutations: + - setjson: | + { "uid" : "uid(x)", + "ComputerOwner.computers": { + "uid": "_:Computer1", + "dgraph.type": ["Computer"], + "Computer.name": "Comp", + "Computer.owners": [ { "uid": "uid(x)" } ] + } + } + cond: "@if(gt(len(x), 0))" - name: "Add mutation with union" @@ -1711,13 +1790,21 @@ } dgquery: |- query { - x as updateHome(func: uid(0x123)) @filter(type(Home)) { + Parrot1(func: uid(0x124)) @filter(type(Parrot)) { uid } - Parrot3 as Parrot3(func: uid(0x124)) @filter(type(Parrot)) { + Parrot2(func: uid(0x125)) @filter(type(Parrot)) { uid } - Parrot8 as Parrot8(func: uid(0x125)) @filter(type(Parrot)) { + } + qnametouid: | + { + "Parrot1": "0x124", + "Parrot2": "0x125" + } + dgquerysec: |- + query { + x as updateHome(func: uid(0x123)) @filter(type(Home)) { uid } } @@ -1732,21 +1819,21 @@ "Animal.category": "Mammal", "Dog.breed": "German Shephard", "dgraph.type": ["Dog", "Animal"], - "uid": "_:Dog4" + "uid": "_:Dog5" }, { "Animal.category": "Bird", "Parrot.repeatsWords": ["squawk"], "dgraph.type": ["Parrot", "Animal"], - "uid": "_:Parrot5" + "uid": "_:Parrot6" }, { "Character.name": "Han Solo", "Employee.ename": "Han_emp", "dgraph.type": ["Human", "Character", "Employee"], - "uid": "_:Human6" + "uid": "_:Human7" }], "uid": "uid(x)" } - cond: "@if(eq(len(Parrot3), 1) AND gt(len(x), 0))" + cond: "@if(gt(len(x), 0))" - deletejson: | { "Home.members": [ @@ -1756,4 +1843,4 @@ ], "uid": "uid(x)" } - cond: "@if(eq(len(Parrot8), 1) AND gt(len(x), 0))" + cond: "@if(gt(len(x), 0))" diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index bf776a37638..e6e3dc0bae3 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -2179,7 +2179,7 @@ func (t *astType) ImplementingTypes() []Type { // satisfy a valid post. func (t *astType) EnsureNonNulls(obj map[string]interface{}, exclusion string) error { for _, fld := range t.inSchema.schema.Types[t.Name()].Fields { - if fld.Type.NonNull && !isID(fld) && fld.Name != exclusion { + if fld.Type.NonNull && !isID(fld) && fld.Name != exclusion && t.inSchema.customDirectives[t.Name()][fld.Name] == nil { if val, ok := obj[fld.Name]; !ok || val == nil { return errors.Errorf( "type %s requires a value for field %s, but no value present",