diff --git a/graphql/e2e/auth/add_mutation_test.go b/graphql/e2e/auth/add_mutation_test.go index 70be41f6b32..285c6a2928e 100644 --- a/graphql/e2e/auth/add_mutation_test.go +++ b/graphql/e2e/auth/add_mutation_test.go @@ -1002,7 +1002,7 @@ func TestUpsertMutationsWithRBAC(t *testing.T) { require.Error(t, gqlResponse.Errors) require.Equal(t, len(gqlResponse.Errors), 1) require.Contains(t, gqlResponse.Errors[0].Error(), - " GraphQL debug: id Tweets already exists for field id inside type tweet1") + " GraphQL debug: id tweet1 already exists for field id inside type Tweets") } else { common.RequireNoGQLErrors(t, gqlResponse) require.JSONEq(t, tcase.result, string(gqlResponse.Data)) @@ -1164,3 +1164,150 @@ func TestAddMutationWithAuthOnIDFieldHavingInterfaceArg(t *testing.T) { // cleanup common.DeleteGqlType(t, "LibraryMember", map[string]interface{}{}, 1, nil) } + +func TestUpdateMutationWithIDFields(t *testing.T) { + addEmployerParams := &common.GraphQLParams{ + Query: `mutation addEmployer($input: [AddEmployerInput!]!) { + addEmployer(input: $input, upsert: false) { + numUids + } + }`, + Variables: map[string]interface{}{"input": []interface{}{ + map[string]interface{}{ + "company": "ABC tech", + "name": "ABC", + "worker": map[string]interface{}{ + "empId": "E01", + "regNo": 101, + }, + }, map[string]interface{}{ + "company": " XYZ tech", + "name": "XYZ", + "worker": map[string]interface{}{ + "empId": "E02", + "regNo": 102, + }, + }, + }, + }, + } + + gqlResponse := addEmployerParams.ExecuteAsPost(t, common.GraphqlURL) + common.RequireNoGQLErrors(t, gqlResponse) + var resultEmployer struct { + AddEmployer struct { + NumUids int + } + } + err := json.Unmarshal(gqlResponse.Data, &resultEmployer) + require.NoError(t, err) + require.Equal(t, 4, resultEmployer.AddEmployer.NumUids) + + // errors while updating node should be returned in debug mode, + // if type have auth rules defined on it + + tcases := []struct { + name string + query string + variables string + error string + }{{ + name: "update mutation gives error when multiple nodes are selected in filter", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": [ + "ABC", + "XYZ" + ] + } + }, + "set": { + "name": "MNO", + "company": "MNO tech" + } + } + }`, + error: "mutation updateEmployer failed because GraphQL debug: only one node is allowed" + + " in the filter while updating fields with @id directive", + }, { + name: "update mutation gives error when given @id field already exist in some node", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": "ABC" + } + }, + "set": { + "company": "ABC tech" + } + } + }`, + error: "couldn't rewrite mutation updateEmployer because failed to rewrite mutation" + + " payload because GraphQL debug: id ABC tech already exists for field company" + + " inside type Employer", + }, + { + name: "update mutation gives error when multiple nodes are found at nested level" + + "while linking rot object to nested object", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": "ABC" + } + }, + "set": { + "name": "JKL", + "worker":{ + "empId":"E01", + "regNo":102 + } + } + } + }`, + error: "couldn't rewrite mutation updateEmployer because failed to rewrite mutation" + + " payload because multiple nodes found for given xid values, updation not possible", + }, + } + + for _, tcase := range tcases { + t.Run(tcase.name, func(t *testing.T) { + var vars map[string]interface{} + if tcase.variables != "" { + err := json.Unmarshal([]byte(tcase.variables), &vars) + require.NoError(t, err) + } + params := &common.GraphQLParams{ + Query: tcase.query, + Variables: vars, + } + + resp := params.ExecuteAsPost(t, common.GraphqlURL) + require.Equal(t, tcase.error, resp.Errors[0].Error()) + }) + } + + // cleanup + filterEmployer := map[string]interface{}{"name": map[string]interface{}{"in": []string{"ABC", "XYZ"}}} + filterWorker := map[string]interface{}{"empId": map[string]interface{}{"in": []string{"E01", "E02"}}} + common.DeleteGqlType(t, "Employer", filterEmployer, 2, nil) + common.DeleteGqlType(t, "Worker", filterWorker, 2, nil) +} diff --git a/graphql/e2e/auth/auth_test.go b/graphql/e2e/auth/auth_test.go index 01941ab496a..4b19e8a90c7 100644 --- a/graphql/e2e/auth/auth_test.go +++ b/graphql/e2e/auth/auth_test.go @@ -354,7 +354,7 @@ func TestAddMutationWithXid(t *testing.T) { require.Error(t, gqlResponse.Errors) require.Equal(t, len(gqlResponse.Errors), 1) require.Contains(t, gqlResponse.Errors[0].Error(), - "GraphQL debug: id Tweets already exists for field id inside type tweet1") + "GraphQL debug: id tweet1 already exists for field id inside type Tweets") // Clear the tweet. tweet.DeleteByID(t, user, metaInfo) diff --git a/graphql/e2e/auth/debug_off/debugoff_test.go b/graphql/e2e/auth/debug_off/debugoff_test.go index b7715052f6e..155dbce2da2 100644 --- a/graphql/e2e/auth/debug_off/debugoff_test.go +++ b/graphql/e2e/auth/debug_off/debugoff_test.go @@ -142,8 +142,6 @@ func TestAddMutationWithAuthOnIDFieldHavingInterfaceArg(t *testing.T) { gqlResponse := addLibraryMemberParams.ExecuteAsPost(t, common.GraphqlURL) common.RequireNoGQLErrors(t, gqlResponse) - // add SportsMember should return error but in debug mode - // because interface type have auth rules defined on it var resultLibraryMember struct { AddLibraryMember struct { NumUids int @@ -153,6 +151,8 @@ func TestAddMutationWithAuthOnIDFieldHavingInterfaceArg(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, resultLibraryMember.AddLibraryMember.NumUids) + // add SportsMember should return error but in debug mode + // because interface type have auth rules defined on it addSportsMemberParams := &common.GraphQLParams{ Query: `mutation addSportsMember($input: [AddSportsMemberInput!]!) { addSportsMember(input: $input, upsert: false) { @@ -184,6 +184,150 @@ func TestAddMutationWithAuthOnIDFieldHavingInterfaceArg(t *testing.T) { common.DeleteGqlType(t, "LibraryMember", map[string]interface{}{}, 1, nil) } +func TestUpdateMutationWithIDFields(t *testing.T) { + addEmployerParams := &common.GraphQLParams{ + Query: `mutation addEmployer($input: [AddEmployerInput!]!) { + addEmployer(input: $input, upsert: false) { + numUids + } + }`, + Variables: map[string]interface{}{"input": []interface{}{ + map[string]interface{}{ + "company": "ABC tech", + "name": "ABC", + "worker": map[string]interface{}{ + "empId": "E01", + "regNo": 101, + }, + }, map[string]interface{}{ + "company": " XYZ tech", + "name": "XYZ", + "worker": map[string]interface{}{ + "empId": "E02", + "regNo": 102, + }, + }, + }, + }, + } + + gqlResponse := addEmployerParams.ExecuteAsPost(t, common.GraphqlURL) + common.RequireNoGQLErrors(t, gqlResponse) + type resEmployer struct { + AddEmployer struct { + NumUids int + } + } + var resultEmployer resEmployer + err := json.Unmarshal(gqlResponse.Data, &resultEmployer) + require.NoError(t, err) + require.Equal(t, 4, resultEmployer.AddEmployer.NumUids) + + // errors while updating node should be returned in debug mode, + // if type have auth rules defined on it + + tcases := []struct { + name string + query string + variables string + error string + }{{ + name: "update mutation gives error when multiple nodes are selected in filter", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": [ + "ABC", + "XYZ" + ] + } + }, + "set": { + "name": "MNO", + "company": "MNO tech" + } + } + }`, + }, { + name: "update mutation gives error when given @id field already exist in some node", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": "ABC" + } + }, + "set": { + "company": "ABC tech" + } + } + }`, + }, + { + name: "update mutation gives error when multiple nodes are found at nested level" + + "while linking rot object to nested object", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + numUids + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": "ABC" + } + }, + "set": { + "name": "JKL", + "worker":{ + "empId":"E01", + "regNo":102 + } + } + } + }`, + }, + } + + for _, tcase := range tcases { + t.Run(tcase.name, func(t *testing.T) { + var vars map[string]interface{} + var resultEmployerErr resEmployer + if tcase.variables != "" { + err := json.Unmarshal([]byte(tcase.variables), &vars) + require.NoError(t, err) + } + params := &common.GraphQLParams{ + Query: tcase.query, + Variables: vars, + } + + resp := params.ExecuteAsPost(t, common.GraphqlURL) + err := json.Unmarshal(resp.Data, &resultEmployerErr) + require.NoError(t, err) + require.Equal(t, 0, resultEmployerErr.AddEmployer.NumUids) + }) + } + + // cleanup + filterEmployer := map[string]interface{}{"name": map[string]interface{}{"in": []string{"ABC", "XYZ"}}} + filterWorker := map[string]interface{}{"empId": map[string]interface{}{"in": []string{"E01", "E02"}}} + common.DeleteGqlType(t, "Employer", filterEmployer, 2, nil) + common.DeleteGqlType(t, "Worker", filterWorker, 2, nil) +} + func TestMain(m *testing.M) { schemaFile := "../schema.graphql" schema, err := os.ReadFile(schemaFile) diff --git a/graphql/e2e/auth/schema.graphql b/graphql/e2e/auth/schema.graphql index b9b928f655b..a9542def0a7 100644 --- a/graphql/e2e/auth/schema.graphql +++ b/graphql/e2e/auth/schema.graphql @@ -864,6 +864,21 @@ type Person name: String! } +type Employer@auth( + query: { rule: "{$ROLE: { eq: \"ADMIN\" } }" }, +) { + company: String! @id + companyId: String @id + name: String @id + worker:[Worker] +} + +type Worker { + regNo: Int @id + uniqueId: Int @id + empId: String! @id +} + interface Member @auth( query: { rule: "{$ROLE: { eq: \"ADMIN\" } }" }, ){ diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index 635fe388437..400049980aa 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -930,7 +930,8 @@ func RunAll(t *testing.T) { t.Run("multiple external Id's tests", multipleXidsTests) t.Run("Upsert Mutation Tests", upsertMutationTests) t.Run("Update language tag fields", updateLangTagFields) - t.Run("add mutation with @id field and interface arg", addMutationWithIDFieldHavingInterfaceArg) + t.Run("mutation with @id field and interface arg", addMutationWithIDFieldHavingInterfaceArg) + t.Run("xid update and nullable tests", xidUpdateAndNullableTests) // error tests t.Run("graphql completion on", graphQLCompletionOn) diff --git a/graphql/e2e/common/mutation.go b/graphql/e2e/common/mutation.go index 07c1675519d..96c3c365dc9 100644 --- a/graphql/e2e/common/mutation.go +++ b/graphql/e2e/common/mutation.go @@ -5540,11 +5540,11 @@ func multipleXidsTests(t *testing.T) { { name: "add worker with multiple xids", query: `mutation { - addWorker(input: [{ name: "Alice", reg_No: 1, emp_Id: "E01" }]) { + addWorker(input: [{ name: "Alice", regNo: 1, empId: "E01" }]) { worker { name - reg_No - emp_Id + regNo + empId } } }`, @@ -5553,60 +5553,63 @@ func multipleXidsTests(t *testing.T) { "worker": [ { "name": "Alice", - "reg_No": 1, - "emp_Id": "E01" + "regNo": 1, + "empId": "E01" } ] } }`, }, { - name: "adding worker with same reg_No will return error", + name: "adding worker with same regNo will return error", query: `mutation { - addWorker(input: [{ name: "Alice", reg_No: 1, emp_Id: "E012" }]) { + addWorker(input: [{ name: "Alice", regNo: 1, empId: "E012" }]) { worker { name - reg_No - emp_Id + regNo + empId } } }`, - error: `couldn't rewrite mutation addWorker because failed to rewrite mutation payload because id 1 already exists for field reg_No inside type Worker`, + error: "couldn't rewrite mutation addWorker because failed to rewrite mutation" + + " payload because id 1 already exists for field regNo inside type Worker", }, { - name: "adding worker with same emp_Id will return error", + name: "adding worker with same empId will return error", query: `mutation { - addWorker(input: [{ name: "Alice", reg_No: 2, emp_Id: "E01" }]) { + addWorker(input: [{ name: "Alice", regNo: 2, empId: "E01" }]) { worker { name - reg_No - emp_Id + regNo + empId } } }`, - error: `couldn't rewrite mutation addWorker because failed to rewrite mutation payload because id E01 already exists for field emp_Id inside type Worker`, + error: "couldn't rewrite mutation addWorker because failed to rewrite mutation" + + " payload because id E01 already exists for field empId inside type Worker", }, { - name: "adding worker with same reg_No and emp_id will return error", + name: "adding worker with same regNo and empId will return error", query: `mutation { - addWorker(input: [{ name: "Alice", reg_No: 1, emp_Id: "E01" }]) { + addWorker(input: [{ name: "Alice", regNo: 1, empId: "E01" }]) { worker { name - reg_No - emp_Id + regNo + empId } } }`, - error: `couldn't rewrite mutation addWorker because failed to rewrite mutation payload because id E01 already exists for field emp_Id inside type Worker`, + error: "couldn't rewrite mutation addWorker because failed to rewrite mutation" + + " payload because id E01 already exists for field empId inside type Worker", }, { - name: "adding worker with different reg_No and emp_id will succeed", + name: "adding worker with different regNo and empId will succeed", query: `mutation { - addWorker(input: [{ name: "Bob", reg_No: 2, emp_Id: "E02" }]) { + addWorker(input: [{ name: "Bob", regNo: 2, empId: "E02" }]) { worker { name - reg_No - emp_Id + regNo + empId } } }`, @@ -5615,27 +5618,27 @@ func multipleXidsTests(t *testing.T) { "worker": [ { "name": "Bob", - "reg_No": 2, - "emp_Id": "E02" + "regNo": 2, + "empId": "E02" } ] } }`, }, { - name: "adding worker with same reg_No and emp_id at deeper level will add reference", + name: "adding worker with same regNo and empId at deeper level will add reference", query: `mutation { addEmployer( input: [ - { company: "Dgraph", worker: { name: "Bob", reg_No: 2, emp_Id: "E02" } } + { company: "Dgraph", worker: { name: "Bob", regNo: 2, empId: "E02" } } ] ) { employer { company worker { name - reg_No - emp_Id + regNo + empId } } } @@ -5648,8 +5651,8 @@ func multipleXidsTests(t *testing.T) { "worker": [ { "name": "Bob", - "reg_No": 2, - "emp_Id": "E02" + "regNo": 2, + "empId": "E02" } ] } @@ -5658,19 +5661,23 @@ func multipleXidsTests(t *testing.T) { }`, }, { - name: "adding worker with different reg_No and emp_id at deep level will add new node", + name: "adding worker with different regNo and empId at deep level will add new node", query: `mutation { - addEmployer(input: [{ company: "GraphQL", worker: { name: "Jack", reg_No: 3, emp_Id: "E03" } }]) { - employer { - company - worker { - name - reg_No - emp_Id - } - } - } - }`, + addEmployer( + input: [ + { company: "GraphQL", worker: { name: "Jack", regNo: 3, empId: "E03" } } + ] + ) { + employer { + company + worker { + name + regNo + empId + } + } + } + }`, expected: `{ "addEmployer": { "employer": [ @@ -5678,8 +5685,8 @@ func multipleXidsTests(t *testing.T) { "worker": [ { "name": "Jack", - "reg_No": 3, - "emp_Id": "E03" + "regNo": 3, + "empId": "E03" } ] } @@ -5688,15 +5695,15 @@ func multipleXidsTests(t *testing.T) { }`, }, { - name: "adding worker with same reg_No but different emp_id at deep level will add reference", + name: "adding worker with same regNo but different empId at deep level will add reference", query: `mutation { - addEmployer(input: [{ company: "Slash", worker: { reg_No: 3, emp_Id: "E04" } }]) { + addEmployer(input: [{ company: "Slash", worker: { regNo: 3, empId: "E04" } }]) { employer { company worker { name - reg_No - emp_Id + regNo + empId } } } @@ -5708,8 +5715,8 @@ func multipleXidsTests(t *testing.T) { "worker": [ { "name": "Jack", - "reg_No": 3, - "emp_Id": "E03" + "regNo": 3, + "empId": "E03" } ] } @@ -5720,51 +5727,51 @@ func multipleXidsTests(t *testing.T) { { name: "get query with multiple Id's", query: `query { - getWorker(reg_No: 2, emp_Id: "E02") { + getWorker(regNo: 2, empId: "E02") { name - reg_No - emp_Id + regNo + empId } }`, expected: `{ "getWorker": { - "emp_Id": "E02", + "empId": "E02", "name": "Bob", - "reg_No": 2 + "regNo": 2 } }`, }, { - name: "query with reg_no", + name: "query with regNo", query: `query { - getWorker(reg_No: 2) { + getWorker(regNo: 2) { name - reg_No - emp_Id + regNo + empId } }`, expected: `{ "getWorker": { - "emp_Id": "E02", + "empId": "E02", "name": "Bob", - "reg_No": 2 + "regNo": 2 } }`, }, { - name: "query with emp_Id", + name: "query with empId", query: `query { - getWorker(emp_Id: "E02") { + getWorker(empId: "E02") { name - reg_No - emp_Id + regNo + empId } }`, expected: `{ "getWorker": { - "emp_Id": "E02", + "empId": "E02", "name": "Bob", - "reg_No": 2 + "regNo": 2 } }`, }, @@ -5772,24 +5779,24 @@ func multipleXidsTests(t *testing.T) { name: "query with multiple Id's using filters", query: `query { queryWorker( - filter: { or: [{ reg_No: { in: 2 } }, { emp_Id: { in: "E01" } }] } + filter: { or: [{ regNo: { in: 2 } }, { empId: { in: "E01" } }] } ) { name - reg_No - emp_Id + regNo + empId } }`, expected: `{ "queryWorker": [ { - "emp_Id": "E02", + "empId": "E02", "name": "Bob", - "reg_No": 2 + "regNo": 2 }, { - "emp_Id": "E01", + "empId": "E01", "name": "Alice", - "reg_No": 1 + "regNo": 1 } ] }`, @@ -5799,9 +5806,9 @@ func multipleXidsTests(t *testing.T) { query: `mutation updateWorker($patch: UpdateWorkerInput!) { updateWorker(input: $patch) { worker { - emp_Id + empId name - reg_No + regNo } } }`, @@ -5809,14 +5816,14 @@ func multipleXidsTests(t *testing.T) { "updateWorker": { "worker": [ { - "emp_Id": "E01", + "empId": "E01", "name": "Jacob", - "reg_No": 1 + "regNo": 1 }, { - "emp_Id": "E02", + "empId": "E02", "name": "Jacob", - "reg_No": 2 + "regNo": 2 } ] } @@ -5825,11 +5832,11 @@ func multipleXidsTests(t *testing.T) { "patch": { "filter": {"or": [ { - "reg_No": {"in": 1 + "regNo": {"in": 1 } }, { - "emp_Id": {"in": "E02" + "empId": {"in": "E02" } } ] @@ -5846,15 +5853,15 @@ func multipleXidsTests(t *testing.T) { updateEmployer( input: { filter: { company: { in: "GraphQL" } } - set: { worker: { name: "Leo", emp_Id: "E06", reg_No: 6 } } + set: { worker: { name: "Leo", empId: "E06", regNo: 6 } } } ) { employer { company worker { - emp_Id + empId name - reg_No + regNo } } } @@ -5866,14 +5873,14 @@ func multipleXidsTests(t *testing.T) { "company": "GraphQL", "worker": [ { - "emp_Id": "E06", + "empId": "E06", "name": "Leo", - "reg_No": 6 + "regNo": 6 }, { - "emp_Id": "E03", + "empId": "E03", "name": "Jack", - "reg_No": 3 + "regNo": 3 } ] } @@ -5882,25 +5889,27 @@ func multipleXidsTests(t *testing.T) { }`, }, { - name: "Deep level update mutation return error when some xids are missing while creating new node using set", + name: "Deep level update mutation return error when non- nullable xids are" + + " missing while creating new node using set", query: `mutation { updateEmployer( input: { filter: { company: { in: "GraphQL" } } - set: { worker: { name: "Leo", emp_Id: "E07" } } + set: { worker: { empId: "E07" } } } ) { employer { company worker { - emp_Id + empId name - reg_No + regNo } } } }`, - error: `couldn't rewrite mutation updateEmployer because failed to rewrite mutation payload because field reg_No cannot be empty`, + error: "couldn't rewrite mutation updateEmployer because failed to rewrite mutation" + + " payload because type Worker requires a value for field name, but no value present", }, } @@ -5923,7 +5932,7 @@ func multipleXidsTests(t *testing.T) { }) } - filter := map[string]interface{}{"reg_No": map[string]interface{}{"in": []int{1, 2, 3, 6}}} + filter := map[string]interface{}{"regNo": map[string]interface{}{"in": []int{1, 2, 3, 6}}} DeleteGqlType(t, "Worker", filter, 4, nil) } @@ -6274,6 +6283,64 @@ func addMutationWithIDFieldHavingInterfaceArg(t *testing.T) { " payload because id 102 already exists for field refID in some other implementing" + " type of interface Member", }, + { + name: "updating inherited @id with interface argument true," + + "returns error if given value for id already exist in a node of " + + "some other implementing type", + query: `mutation update($patch: UpdateLibraryMemberInput!) { + updateLibraryMember(input: $patch) { + libraryMember { + refID + } + } + }`, + variables: `{ + "patch": { + "filter": { + "refID": { + "in": "101" + } + }, + "set": { + "refID": "102", + "name": "Miles", + "readHours": "5d2hr" + } + } + }`, + error: "couldn't rewrite mutation updateLibraryMember because failed to rewrite" + + " mutation payload because id 102 already exists for field refID in some other" + + " implementing type of interface Member", + }, + { + name: "updating link to a type that have inherited @id field with interface" + + " argument true, returns error if given value for id field already exist" + + " in a node of some other implementing type", + query: `mutation update($patch: UpdateLibraryManagerInput!) { + updateLibraryManager(input: $patch) { + libraryManager { + name + } + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": "Juliet" + } + }, + "set": { + "manages": { + "refID": "102" + } + } + } + }`, + error: "couldn't rewrite mutation updateLibraryManager because failed to rewrite mutation" + + " payload because id 102 already exists for field refID in some other" + + " implementing type of interface Member", + }, } for _, tcase := range tcases { @@ -6303,3 +6370,266 @@ func addMutationWithIDFieldHavingInterfaceArg(t *testing.T) { DeleteGqlType(t, "CricketTeam", map[string]interface{}{}, 1, nil) DeleteGqlType(t, "LibraryManager", map[string]interface{}{}, 1, nil) } + +func xidUpdateAndNullableTests(t *testing.T) { + tcases := []struct { + name string + query string + variables string + error string + }{ + { + name: "2-level add mutation without nullable @id fields", + query: `mutation addEmployer($input: [AddEmployerInput!]!) { + addEmployer(input: $input, upsert: false) { + employer { + company + } + } + }`, + variables: `{ + "input": [ + { + "company": "ABC tech", + "name": "XYZ", + "worker": { + "name": "Alice", + "regNo": 101, + "empId": "E01" + } + }, + { + "company": "XYZ industry", + "name": "ABC", + "worker": { + "name": "Bob", + "regNo": 102, + "empId": "E02" + } + } + ] + }`, + }, { + name: "2-level add mutation with upserts without nullable @id fields", + query: `mutation addEmployer($input: [AddEmployerInput!]!) { + addEmployer(input: $input, upsert: true) { + employer { + company + } + } + }`, + variables: `{ + "input": { + "company": "ABC tech", + "worker": { + "name": "Juliet", + "regNo": 103, + "empId": "E03" + } + } + }`, + }, { + name: "upsert mutation gives error when multiple nodes are found with given @id fields", + query: `mutation addEmployer($input: [AddEmployerInput!]!) { + addEmployer(input: $input, upsert: true) { + employer { + company + } + } + }`, + variables: `{ + "input": { + "company": "ABC tech", + "name": "ABC" + } + }`, + error: "couldn't rewrite mutation addEmployer because failed to rewrite mutation" + + " payload because multiple nodes found for given xid values, updation not possible", + }, { + name: "upsert mutation gives error when multiple nodes are found with" + + " given @id fields at nested level", + query: `mutation addEmployer($input: [AddEmployerInput!]!) { + addEmployer(input: $input, upsert: true) { + employer { + company + } + } + }`, + variables: `{ + "input": { + "company": "ABC tech", + "worker": { + "empId": "E02", + "regNo": 103, + "name": "William" + } + } + }`, + error: "couldn't rewrite mutation addEmployer because failed to rewrite mutation" + + " payload because multiple nodes found for given xid values, updation not possible", + }, + { + name: "Non-nullable id should be present while creating new node at nested level" + + " using upsert", + query: `mutation addEmployer($input: [AddEmployerInput!]!) { + addEmployer(input: $input, upsert: true) { + employer { + company + } + } + }`, + variables: `{ + "input": { + "company": "ABC tech1", + "worker": { + "regNo": 104, + "name": "John" + } + } + }`, + error: "couldn't rewrite mutation addEmployer because failed to rewrite" + + " mutation payload because type Worker requires a value for" + + " field empId, but no value present", + }, + { + name: "update mutation fails when @id field is being updated" + + " and multiple nodes are selected in filter", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + employer { + company + } + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": [ + "XYZ", + "ABC" + ] + } + }, + "set": { + "company": "JKL" + } + } + }`, + error: "mutation updateEmployer failed because only one node is allowed in the filter" + + " while updating fields with @id directive", + }, { + name: "successfully updating @id field of a node ", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + employer { + company + } + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": [ + "XYZ" + ] + } + }, + "set": { + "name": "JKL", + "company": "JKL tech" + } + } + }`, + }, + { + name: "updating @id field returns error because given value in update mutation already exists", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + employer { + company + } + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": [ + "JKL" + ] + } + }, + "set": { + "name": "ABC", + "company": "ABC tech" + } + } + }`, + error: "couldn't rewrite mutation updateEmployer because failed to rewrite mutation" + + " payload because id ABC already exists for field name inside type Employer", + }, + { + name: "updating root @id fields and also create a nested link to nested object", + query: `mutation update($patch: UpdateEmployerInput!) { + updateEmployer(input: $patch) { + employer { + company + } + } + }`, + variables: `{ + "patch": { + "filter": { + "name": { + "in": [ + "JKL" + ] + } + }, + "set": { + "name": "MNO", + "company": "MNO tech", + "worker": { + "name": "Miles", + "empId": "E05", + "regNo": 105 + } + } + } + }`, + }, + } + + for _, tcase := range tcases { + t.Run(tcase.name, func(t *testing.T) { + var vars map[string]interface{} + if tcase.variables != "" { + err := json.Unmarshal([]byte(tcase.variables), &vars) + require.NoError(t, err) + } + params := &GraphQLParams{ + Query: tcase.query, + Variables: vars, + } + resp := params.ExecuteAsPost(t, GraphqlURL) + if tcase.error != "" { + require.Equal(t, tcase.error, resp.Errors[0].Error()) + } else { + RequireNoGQLErrors(t, resp) + } + + }) + } + + // Cleanup + filterEmployer := + map[string]interface{}{ + "name": map[string]interface{}{"in": []string{"ABC", "MNO"}}} + filterWorker := + map[string]interface{}{ + "regNo": map[string]interface{}{"in": []int{101, 102, 103, 105}}} + DeleteGqlType(t, "Employer", filterEmployer, 2, nil) + DeleteGqlType(t, "Worker", filterWorker, 4, nil) +} diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index 25b6342feb3..f7e6718608b 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -10,19 +10,19 @@ type Hotel { } type Country { - # **Don't delete** Comments in types should work - id: ID! # **Don't delete** Comments in lines should work - name: String! @search(by: [trigram, hash]) - states: [State] @hasInverse(field: country) @dgraph(pred: "hasStates") + # **Don't delete** Comments in types should work + id: ID! # **Don't delete** Comments in lines should work + name: String! @search(by: [trigram, hash]) + states: [State] @hasInverse(field: country) @dgraph(pred: "hasStates") } type State { - id: ID! - xcode: String! @id @search(by: [regexp]) - name: String! - capital: String - region: Region - country: Country @dgraph(pred: "inCountry") + id: ID! + xcode: String! @id @search(by: [regexp]) + name: String! + capital: String + region: Region + country: Country @dgraph(pred: "inCountry") } # **Don't delete** Comments in the middle of schemas should work @@ -34,45 +34,45 @@ GraphQL descriptions look like this. They should work in the input schema and should make their way into the generated schema. """ type Author @dgraph(type: "test.dgraph.author") { - id: ID! + id: ID! - """ - GraphQL descriptions can be on fields. They should work in the input - schema and should make their way into the generated schema. - """ - name: String! @search(by: [hash, trigram]) + """ + GraphQL descriptions can be on fields. They should work in the input + schema and should make their way into the generated schema. + """ + name: String! @search(by: [hash, trigram]) - dob: DateTime @search - reputation: Float @search - qualification: String @search(by: [hash, trigram]) - country: Country - posts: [Post!] @hasInverse(field: author) - bio: String @lambda - rank: Int @lambda + dob: DateTime @search + reputation: Float @search + qualification: String @search(by: [hash, trigram]) + country: Country + posts: [Post!] @hasInverse(field: author) + bio: String @lambda + rank: Int @lambda } type Post @dgraph(type: "myPost") { - postID: ID! - title: String! @search(by: [term, fulltext]) - text: String @search(by: [fulltext]) @dgraph(pred: "text") - tags: [String] @search(by: [exact]) - topic: String @search(by: [exact]) @dgraph(pred: "test.dgraph.topic") - numLikes: Int @search - numViews: Int64 @search - isPublished: Boolean @search @dgraph(pred: "is_published") - postType: PostType @search(by: [hash, trigram]) - author: Author! @hasInverse(field: posts) @dgraph(pred: "post.author") - category: Category @hasInverse(field: posts) + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext]) @dgraph(pred: "text") + tags: [String] @search(by: [exact]) + topic: String @search(by: [exact]) @dgraph(pred: "test.dgraph.topic") + numLikes: Int @search + numViews: Int64 @search + isPublished: Boolean @search @dgraph(pred: "is_published") + postType: PostType @search(by: [hash, trigram]) + author: Author! @hasInverse(field: posts) @dgraph(pred: "post.author") + category: Category @hasInverse(field: posts) } type Category { - id: ID - name: String - posts: [Post] + id: ID + name: String + posts: [Post] } type User @secret(field: "password", pred:"pwd"){ - name: String! @id + name: String! @id } """ @@ -80,14 +80,14 @@ GraphQL descriptions can be on enums. They should work in the input schema and should make their way into the generated schema. """ enum PostType { - Fact + Fact - """ - GraphQL descriptions can be on enum values. They should work in the input - schema and should make their way into the generated schema. - """ - Question - Opinion + """ + GraphQL descriptions can be on enum values. They should work in the input + schema and should make their way into the generated schema. + """ + Question + Opinion } """ @@ -95,71 +95,71 @@ GraphQL descriptions can be on interfaces. They should work in the input schema and should make their way into the generated schema. """ interface Employee @dgraph(type: "test.dgraph.employee.en") { - ename: String! + ename: String! } interface Character @dgraph(type: "performance.character") { - id: ID! - name: String! @search(by: [exact]) - appearsIn: [Episode!] @search @dgraph(pred: "appears_in") - bio: String @lambda + id: ID! + name: String! @search(by: [exact]) + appearsIn: [Episode!] @search @dgraph(pred: "appears_in") + bio: String @lambda } type Human implements Character & Employee { - id: ID! - name: String! @search(by: [exact]) - appearsIn: [Episode!] @search - bio: String @lambda - ename: String! - starships: [Starship] - totalCredits: Float @dgraph(pred: "credits") + id: ID! + name: String! @search(by: [exact]) + appearsIn: [Episode!] @search + bio: String @lambda + ename: String! + starships: [Starship] + totalCredits: Float @dgraph(pred: "credits") } type Droid implements Character @dgraph(type: "roboDroid") { - id: ID! - name: String! @search(by: [exact]) - appearsIn: [Episode!] @search - bio: String @lambda - primaryFunction: String + id: ID! + name: String! @search(by: [exact]) + appearsIn: [Episode!] @search + bio: String @lambda + primaryFunction: String } enum Episode { - NEWHOPE - EMPIRE - JEDI + NEWHOPE + EMPIRE + JEDI } type Starship @dgraph(type: "star.ship") { - id: ID! - name: String! @search(by: [term]) @dgraph(pred: "star.ship.name") - length: Float + id: ID! + name: String! @search(by: [term]) @dgraph(pred: "star.ship.name") + length: Float } type Movie { - id: ID! - name: String! - director: [MovieDirector] @dgraph(pred: "~directed.movies") + id: ID! + name: String! + director: [MovieDirector] @dgraph(pred: "~directed.movies") } type MovieDirector { - id: ID! - name: String! - directed: [Movie] @dgraph(pred: "directed.movies") + id: ID! + name: String! + directed: [Movie] @dgraph(pred: "directed.movies") } interface People { - id: ID! - xid: String! @id - name: String! + id: ID! + xid: String! @id + name: String! } type Teacher implements People { - subject: String - teaches: [Student] + subject: String + teaches: [Student] } type Student implements People { - taughtBy: [Teacher] @hasInverse(field: "teaches") + taughtBy: [Teacher] @hasInverse(field: "teaches") } type Message @withSubscription { @@ -171,29 +171,29 @@ type Message @withSubscription { This is used for fragment related testing """ interface Thing { - name: String # field to act as a common inherited field for both ThingOne and ThingTwo + name: String # field to act as a common inherited field for both ThingOne and ThingTwo } type ThingOne implements Thing { - id: ID! # ID field with same name as the ID field in ThingTwo - color: String # field with same name as a field in ThingTwo - usedBy: String # field with different name than any field in ThingTwo + id: ID! # ID field with same name as the ID field in ThingTwo + color: String # field with same name as a field in ThingTwo + usedBy: String # field with different name than any field in ThingTwo } type ThingTwo implements Thing { - id: ID! - color: String - owner: String + id: ID! + color: String + owner: String } type Post1 { - id: String! @id - comments: [Comment1] + id: String! @id + comments: [Comment1] } type Comment1 { - id: String! @id - replies: [Comment1] + id: String! @id + replies: [Comment1] } type post1{ id: ID @@ -225,29 +225,29 @@ type Person { # union testing - start enum AnimalCategory { - Fish - Amphibian - Reptile - Bird - Mammal - InVertebrate + Fish + Amphibian + Reptile + Bird + Mammal + InVertebrate } interface Animal { - id: ID! - category: AnimalCategory @search + id: ID! + category: AnimalCategory @search } type Dog implements Animal { - breed: String @search + breed: String @search } type Parrot implements Animal { - repeatsWords: [String] + repeatsWords: [String] } type Cheetah implements Animal { - speed: Float + speed: Float } """ @@ -255,32 +255,32 @@ This type specifically doesn't implement any interface. We need this to test out all cases with union. """ type Plant { - id: ID! - breed: String # field with same name as a field in type Dog + id: ID! + breed: String # field with same name as a field in type Dog } union HomeMember = Dog | Parrot | Human | Plant type Zoo { - id: ID! - animals: [Animal] - city: String + id: ID! + animals: [Animal] + city: String } type Home { - id: ID! - address: String - members: [HomeMember] - favouriteMember: HomeMember + id: ID! + address: String + members: [HomeMember] + favouriteMember: HomeMember } # union testing - end type Query { - authorsByName(name: String!): [Author] @lambda + authorsByName(name: String!): [Author] @lambda } type Mutation { - newAuthor(name: String!): ID! @lambda + newAuthor(name: String!): ID! @lambda } # generate directive testing @@ -304,14 +304,14 @@ type Book { bookId: Int64! @id name: String! desc: String - summary: String @lambda - chapters: [Chapter] @hasInverse(field: book) + summary: String @lambda + chapters: [Chapter] @hasInverse(field: book) } type Chapter { chapterId: Int! @id name: String! - book: Book + book: Book } type Mission @key(fields: "id") { @@ -338,8 +338,8 @@ type SpaceShip @key(fields: "id") @extends { } type Planet @key(fields: "id") @extends { - id: Int! @id @external - missions: [Mission] + id: Int! @id @external + missions: [Mission] } type Region { @@ -381,12 +381,15 @@ type author1{ # multiple fields with @id directive type Worker { name: String! - reg_No: Int! @id - emp_Id: String! @id + regNo: Int @id + uniqueId: Int @id + empId: String! @id } type Employer { company: String! @id + companyId: String @id + name: String @id worker: [Worker] } diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index 683460f7da3..b41cb67668b 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -37,7 +37,7 @@ }, { "index": true, - "predicate": "Worker.emp_Id", + "predicate": "Worker.empId", "tokenizer": [ "hash" ], @@ -46,7 +46,7 @@ }, { "index": true, - "predicate": "Worker.reg_No", + "predicate": "Worker.regNo", "tokenizer": [ "int" ], @@ -178,6 +178,34 @@ "predicate": "Employer.worker", "type": "uid" }, + { + "index": true, + "predicate": "Employer.name", + "tokenizer": [ + "hash" + ], + "type": "string", + "upsert": true + }, + { + "index": true, + "predicate": "Worker.uniqueId", + "tokenizer": [ + "int" + ], + "type": "int", + "upsert": true + }, + { + "index": true, + "index": true, + "predicate": "Employer.companyId", + "tokenizer": [ + "hash" + ], + "type": "string", + "upsert": true + }, { "lang": true, "predicate": "Person.profession", @@ -1031,13 +1059,16 @@ { "fields": [ { - "name": "Worker.emp_Id" + "name": "Worker.empId" }, { - "name": "Worker.reg_No" + "name": "Worker.regNo" }, { "name": "Worker.name" + }, + { + "name": "Worker.uniqueId" } ], "name": "Worker" @@ -1533,6 +1564,12 @@ }, { "name": "Employer.worker" + }, + { + "name": "Employer.companyId" + }, + { + "name": "Employer.name" } ], "name": "Employer" diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index 78d0011ca30..396dea4318b 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -2,27 +2,27 @@ # See: https://github.com/dgraph-io/dgraph/issues/4227 type Hotel { - id: ID! - name: String! @search(by: [exact]) - location: Point @search - area: Polygon @search - branches: MultiPolygon @search + id: ID! + name: String! @search(by: [exact]) + location: Point @search + area: Polygon @search + branches: MultiPolygon @search } type Country { - # **Don't delete** Comments in types should work - id: ID! # **Don't delete** Comments in in lines should work - name: String! @search(by: [trigram, hash]) - states: [State] @hasInverse(field: country) + # **Don't delete** Comments in types should work + id: ID! # **Don't delete** Comments in in lines should work + name: String! @search(by: [trigram, hash]) + states: [State] @hasInverse(field: country) } type State { - id: ID! - xcode: String! @id @search(by: [regexp]) - name: String! - capital: String - region: Region - country: Country + id: ID! + xcode: String! @id @search(by: [regexp]) + name: String! + capital: String + region: Region + country: Country } # **Don't delete** Comments in the middle of schemas should work @@ -34,45 +34,45 @@ GraphQL descriptions look like this. They should work in the input schema and should make their way into the generated schema. """ type Author { - id: ID! + id: ID! - """ - GraphQL descriptions can be on fields. They should work in the input - schema and should make their way into the generated schema. - """ - name: String! @search(by: [hash, trigram]) + """ + GraphQL descriptions can be on fields. They should work in the input + schema and should make their way into the generated schema. + """ + name: String! @search(by: [hash, trigram]) - dob: DateTime @search - reputation: Float @search - qualification: String @search(by: [hash, trigram]) - country: Country - posts: [Post!] @hasInverse(field: author) - bio: String @lambda - rank: Int @lambda + dob: DateTime @search + reputation: Float @search + qualification: String @search(by: [hash, trigram]) + country: Country + posts: [Post!] @hasInverse(field: author) + bio: String @lambda + rank: Int @lambda } type Post { - postID: ID! - title: String! @search(by: [term, fulltext]) - text: String @search(by: [fulltext]) - tags: [String] @search(by: [exact]) - topic: String @search(by: [exact]) - numLikes: Int @search - numViews: Int64 @search - isPublished: Boolean @search - postType: PostType @search(by: [hash, trigram]) - author: Author! @hasInverse(field: posts) - category: Category @hasInverse(field: posts) + postID: ID! + title: String! @search(by: [term, fulltext]) + text: String @search(by: [fulltext]) + tags: [String] @search(by: [exact]) + topic: String @search(by: [exact]) + numLikes: Int @search + numViews: Int64 @search + isPublished: Boolean @search + postType: PostType @search(by: [hash, trigram]) + author: Author! @hasInverse(field: posts) + category: Category @hasInverse(field: posts) } type Category { - id: ID - name: String - posts: [Post] + id: ID + name: String + posts: [Post] } type User @secret(field: "password") { - name: String! @id + name: String! @id } """ @@ -80,14 +80,14 @@ GraphQL descriptions can be on enums. They should work in the input schema and should make their way into the generated schema. """ enum PostType { - Fact + Fact - """ - GraphQL descriptions can be on enum values. They should work in the input - schema and should make their way into the generated schema. - """ - Question - Opinion + """ + GraphQL descriptions can be on enum values. They should work in the input + schema and should make their way into the generated schema. + """ + Question + Opinion } """ @@ -95,126 +95,126 @@ GraphQL descriptions can be on interfaces. They should work in the input schema and should make their way into the generated schema. """ interface Employee { - ename: String! + ename: String! } interface Character { - id: ID! - name: String! @search(by: [exact]) - appearsIn: [Episode!] @search - bio: String @lambda + id: ID! + name: String! @search(by: [exact]) + appearsIn: [Episode!] @search + bio: String @lambda } type Human implements Character & Employee { - id: ID! - name: String! @search(by: [exact]) - appearsIn: [Episode!] @search - bio: String @lambda - ename: String! - starships: [Starship] - totalCredits: Float + id: ID! + name: String! @search(by: [exact]) + appearsIn: [Episode!] @search + bio: String @lambda + ename: String! + starships: [Starship] + totalCredits: Float } type Droid implements Character { - id: ID! - name: String! @search(by: [exact]) - appearsIn: [Episode!] @search - bio: String @lambda - primaryFunction: String + id: ID! + name: String! @search(by: [exact]) + appearsIn: [Episode!] @search + bio: String @lambda + primaryFunction: String } enum Episode { - NEWHOPE - EMPIRE - JEDI + NEWHOPE + EMPIRE + JEDI } type Starship { - id: ID! - name: String! @search(by: [term]) - length: Float + id: ID! + name: String! @search(by: [term]) + length: Float } type Movie { - id: ID! - name: String! - director: [MovieDirector] @hasInverse(field: directed) + id: ID! + name: String! + director: [MovieDirector] @hasInverse(field: directed) } type MovieDirector { - id: ID! - name: String! - directed: [Movie] + id: ID! + name: String! + directed: [Movie] } interface People { - id: ID! - xid: String! @id - name: String! + id: ID! + xid: String! @id + name: String! } type Teacher implements People { - subject: String - teaches: [Student] + subject: String + teaches: [Student] } type Student implements People { - taughtBy: [Teacher] @hasInverse(field: teaches) + taughtBy: [Teacher] @hasInverse(field: teaches) } type Person @withSubscription{ - id: ID! - name: String! @search(by: [hash]) - nameHi: String @dgraph(pred:"Person.name@hi") @search(by: [hash]) - nameZh: String @dgraph(pred:"Person.name@zh") @search(by: [hash]) - nameHiZh: String @dgraph(pred:"Person.name@hi:zh") - nameZhHi: String @dgraph(pred:"Person.name@zh:hi") - nameHi_Zh_Untag: String @dgraph(pred:"Person.name@hi:zh:.") - name_Untag_AnyLang: String @dgraph(pred:"Person.name@.") @search(by: [hash]) - professionEn: String @dgraph(pred:"Person.profession@en") + id: ID! + name: String! @search(by: [hash]) + nameHi: String @dgraph(pred:"Person.name@hi") @search(by: [hash]) + nameZh: String @dgraph(pred:"Person.name@zh") @search(by: [hash]) + nameHiZh: String @dgraph(pred:"Person.name@hi:zh") + nameZhHi: String @dgraph(pred:"Person.name@zh:hi") + nameHi_Zh_Untag: String @dgraph(pred:"Person.name@hi:zh:.") + name_Untag_AnyLang: String @dgraph(pred:"Person.name@.") @search(by: [hash]) + professionEn: String @dgraph(pred:"Person.profession@en") } """ This is used for fragment related testing """ interface Thing { - name: String # field to act as a common inherited field for both ThingOne and ThingTwo + name: String # field to act as a common inherited field for both ThingOne and ThingTwo } type ThingOne implements Thing { - id: ID! # ID field with same name as the ID field in ThingTwo - color: String # field with same name as a field in ThingTwo - usedBy: String # field with different name than any field in ThingTwo + id: ID! # ID field with same name as the ID field in ThingTwo + color: String # field with same name as a field in ThingTwo + usedBy: String # field with different name than any field in ThingTwo } type ThingTwo implements Thing { - id: ID! - color: String - owner: String + id: ID! + color: String + owner: String } type Post1 { - id: String! @id - comments: [Comment1] + id: String! @id + comments: [Comment1] } type Comment1 { - id: String! @id - replies: [Comment1] + id: String! @id + replies: [Comment1] } type author1{ - name:String! @id @search(by: [regexp]) - posts:[post1] @hasInverse(field: author) + name:String! @id @search(by: [regexp]) + posts:[post1] @hasInverse(field: author) } type post1{ - id: ID - title: String! @id @search(by: [regexp]) - numLikes: Int64 @search - commentsByMonth: [Int] - likesByMonth: [Int64] - author:author1 @hasInverse(field: posts) + id: ID + title: String! @id @search(by: [regexp]) + numLikes: Int64 @search + commentsByMonth: [Int] + likesByMonth: [Int64] + author:author1 @hasInverse(field: posts) } type Person1 { @@ -226,29 +226,29 @@ type Person1 { # union testing - start enum AnimalCategory { - Fish - Amphibian - Reptile - Bird - Mammal - InVertebrate + Fish + Amphibian + Reptile + Bird + Mammal + InVertebrate } interface Animal { - id: ID! - category: AnimalCategory @search + id: ID! + category: AnimalCategory @search } type Dog implements Animal { - breed: String @search + breed: String @search } type Parrot implements Animal { - repeatsWords: [String] + repeatsWords: [String] } type Cheetah implements Animal { - speed: Float + speed: Float } """ @@ -256,75 +256,78 @@ This type specifically doesn't implement any interface. We need this to test out all cases with union. """ type Plant { - id: ID! - breed: String # field with same name as a field in type Dog + id: ID! + breed: String # field with same name as a field in type Dog } union HomeMember = Dog | Parrot | Human | Plant type Zoo { - id: ID! - animals: [Animal] - city: String + id: ID! + animals: [Animal] + city: String } type Home { - id: ID! - address: String - members: [HomeMember] - favouriteMember: HomeMember + id: ID! + address: String + members: [HomeMember] + favouriteMember: HomeMember } # union testing - end type Query { - authorsByName(name: String!): [Author] @lambda + authorsByName(name: String!): [Author] @lambda } type Mutation { - newAuthor(name: String!): ID! @lambda + newAuthor(name: String!): ID! @lambda } # generate directive testing type University @generate( - query: { - query: false - }, - mutation: { - add: true, - update: true, - delete: false - } + query: { + query: false + }, + mutation: { + add: true, + update: true, + delete: false + } ){ - id: ID! - name: String! - numStudents: Int + id: ID! + name: String! + numStudents: Int } # @id directive with multiple data types type Book { - bookId: Int64! @id - name: String! - desc: String - summary: String @lambda - chapters: [Chapter] @hasInverse(field: book) + bookId: Int64! @id + name: String! + desc: String + summary: String @lambda + chapters: [Chapter] @hasInverse(field: book) } type Chapter { - chapterId: Int! @id - name: String! - book: Book + chapterId: Int! @id + name: String! + book: Book } # multiple fields with @id directive type Worker { - name: String! - reg_No: Int! @id - emp_Id: String! @id + name: String! + regNo: Int @id + uniqueId: Int @id + empId: String! @id } type Employer { - company: String! @id - worker: [Worker] + company: String! @id + companyId: String @id + name: String @id + worker: [Worker] } # sample data: https://github.com/mandiwise/space-camp-federation-demo/blob/master/db.json @@ -352,71 +355,71 @@ type SpaceShip @key(fields: "id") @extends { } type Planet @key(fields: "id") @extends { - id: Int! @id @external - missions: [Mission] + id: Int! @id @external + missions: [Mission] } type Region { - id: String! @id - name: String! - district: District + id: String! @id + name: String! + district: District } type District @lambdaOnMutate(add: true, update: true, delete: true) { - dgId: ID! - id: String! @id - name: String! + dgId: ID! + id: String! @id + name: String! } type Owner { - username: String! @id - password: String! - projects: [Project!] @hasInverse(field: 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) + 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]) + id: String! @id + owner: Owner! + project: Project! + name: String! @search(by: [hash]) } interface Member { - refID: String! @id (interface:true) - name: String! @id - itemsIssued: [String] - fineAccumulated: Int + refID: String! @id (interface:true) + name: String! @id + itemsIssued: [String] + fineAccumulated: Int } interface Team { - teamID: String! @id (interface:true) - teamName: String! @id + teamID: String! @id (interface:true) + teamName: String! @id } type LibraryMember implements Member { - interests: [String] - readHours: String + interests: [String] + readHours: String } type SportsMember implements Member & Team { - plays: String - playerRating: Int + plays: String + playerRating: Int } type CricketTeam implements Team { - numOfBatsmen: Int - numOfBowlers: Int + numOfBatsmen: Int + numOfBowlers: Int } type LibraryManager { - name: String! @id - manages: [LibraryMember] + name: String! @id + manages: [LibraryMember] } \ No newline at end of file diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index d1befcdc844..310dcbe6279 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -432,7 +432,7 @@ }, { "index": true, - "predicate": "Worker.emp_Id", + "predicate": "Worker.empId", "tokenizer": [ "hash" ], @@ -441,7 +441,7 @@ }, { "index": true, - "predicate": "Worker.reg_No", + "predicate": "Worker.regNo", "tokenizer": [ "int" ], @@ -737,6 +737,34 @@ "predicate": "Employer.worker", "type": "uid" }, + { + "index": true, + "predicate": "Employer.name", + "tokenizer": [ + "hash" + ], + "type": "string", + "upsert": true + }, + { + "index": true, + "predicate": "Worker.uniqueId", + "tokenizer": [ + "int" + ], + "type": "int", + "upsert": true + }, + { + "index": true, + "index": true, + "predicate": "Employer.companyId", + "tokenizer": [ + "hash" + ], + "type": "string", + "upsert": true + }, { "predicate": "Teacher.subject", "type": "string" @@ -975,13 +1003,16 @@ { "fields": [ { - "name": "Worker.emp_Id" + "name": "Worker.empId" }, { - "name": "Worker.reg_No" + "name": "Worker.regNo" }, { "name": "Worker.name" + }, + { + "name": "Worker.uniqueId" } ], "name": "Worker" @@ -1510,6 +1541,12 @@ }, { "name": "Employer.worker" + }, + { + "name": "Employer.companyId" + }, + { + "name": "Employer.name" } ], "name": "Employer" diff --git a/graphql/e2e/subscription/subscription_test.go b/graphql/e2e/subscription/subscription_test.go index 375ed7c150d..f33e87ad39f 100644 --- a/graphql/e2e/subscription/subscription_test.go +++ b/graphql/e2e/subscription/subscription_test.go @@ -82,12 +82,12 @@ const ( timestamp: DateTime @search } type User { - screen_name: String! @id + screenName: String! @id followers: Int @search tweets: [Tweets] @hasInverse(field: author) } type UserTweetCount @remote { - screen_name: String + screenName: String tweetCount: Int } @@ -95,7 +95,7 @@ const ( queryUserTweetCounts: [UserTweetCount] @withSubscription @custom(dql: """ query { queryUserTweetCounts(func: type(User)) { - screen_name: User.screen_name + screenName: User.screenName tweetCount: count(User.tweets) } } @@ -964,7 +964,7 @@ func TestSubscriptionWithCustomDQL(t *testing.T) { add := &common.GraphQLParams{ Query: `mutation { addTweets(input: [ - {text: "Graphql is best",author:{screen_name:"001"}}, + {text: "Graphql is best",author:{screenName:"001"}}, ]) { numUids tweets { @@ -980,7 +980,7 @@ func TestSubscriptionWithCustomDQL(t *testing.T) { subscriptionClient, err := common.NewGraphQLSubscription(subscriptionEndpoint, &schema.Request{ Query: `subscription { queryUserTweetCounts{ - screen_name + screenName tweetCount } }`, @@ -993,7 +993,7 @@ func TestSubscriptionWithCustomDQL(t *testing.T) { require.NoError(t, json.Unmarshal(res, &subscriptionResp)) common.RequireNoGQLErrors(t, &subscriptionResp) - require.JSONEq(t, `{"queryUserTweetCounts":[{"screen_name":"001","tweetCount": 1}]}`, string(subscriptionResp.Data)) + require.JSONEq(t, `{"queryUserTweetCounts":[{"screenName":"001","tweetCount": 1}]}`, string(subscriptionResp.Data)) require.Contains(t, subscriptionResp.Extensions, touchedUidskey) require.Greater(t, int(subscriptionResp.Extensions[touchedUidskey].(float64)), 0) @@ -1001,8 +1001,8 @@ func TestSubscriptionWithCustomDQL(t *testing.T) { add = &common.GraphQLParams{ Query: `mutation { addTweets(input: [ - {text: "Dgraph is best",author:{screen_name:"002"}} - {text: "Badger is best",author:{screen_name:"001"}}, + {text: "Dgraph is best",author:{screenName:"002"}} + {text: "Badger is best",author:{screenName:"001"}}, ]) { numUids tweets { diff --git a/graphql/resolve/add_mutation_test.yaml b/graphql/resolve/add_mutation_test.yaml index 1f85a553e98..8449467e9b9 100644 --- a/graphql/resolve/add_mutation_test.yaml +++ b/graphql/resolve/add_mutation_test.yaml @@ -893,6 +893,7 @@ dgmutations: - setjson: | { "uid" : "uid(State_1)", + "State.code":"nsw", "State.name": "NSW", "State.country": { "uid": "0x12", @@ -915,6 +916,7 @@ - setjson: | { "uid" : "uid(State_3)", "State.name": "Maharashtra", + "State.code": "mh", "State.country": { "uid": "0x14", "Country.states": [ { "uid": "uid(State_3)" } ] @@ -980,6 +982,8 @@ dgmutations: - setjson: | { "uid" : "uid(Book_2)", + "Book.ISBN":"NSW", + "Book.title":"Sapiens", "Book.publisher": "penguin" } cond: "@if(gt(len(Book_2), 0))" @@ -1032,7 +1036,9 @@ - setjson: | { "uid" : "uid(Book_2)", - "Book.publisher": "penguin" + "Book.ISBN":"NSW", + "Book.publisher": "penguin", + "Book.title":"Sapiens" } cond: "@if(gt(len(Book_2), 0))" @@ -1096,6 +1102,7 @@ - setjson: | { "uid" : "uid(State_1)", "State.name": "NSW", + "State.code": "nsw", "State.country": { "uid": "0x12", "Country.states": [ { "uid": "uid(State_1)" } ] @@ -1331,6 +1338,8 @@ dgmutations: - setjson: | { + "Member.name": "Alice", + "Member.refID": "101", "LibraryMember.readHours": "4d2hr", "Member.itemsIssued": [ "Intro to Go", @@ -3739,7 +3748,7 @@ "uid":"_:Author_2" } -- name: "Deep mutation three level xid with no initial XID" +- name: "Deep mutation three level xid with no initial XID " gqlmutation: | mutation($auth: [AddPost1Input!]!) { addPost1(input: $auth) { @@ -4610,12 +4619,12 @@ "dgraph.type": [ "Person1" ], - "uid": "_:Person1_4" + "uid": "_:Person1_3" } ], "Person1.friends": [ { - "uid": "_:Person1_4", + "uid": "_:Person1_3", "Person1.friends": [ { "uid": "_:Person1_2" @@ -4669,46 +4678,16 @@ dgraph.type } } - dgquerysec: |- - query { - var(func: uid(0x117)) { - author_4 as Book.author - } - } qnametouid: | { - "Book_1": "0x117", - "Book_2": "0x116" + "Book_1": "0x12", + "Book_2": "0x11" + } + error2: + { + "message": "failed to rewrite mutation payload because multiple nodes found for given xid values, + updation not possible" } - dgmutations: - - setjson: | - { - "author.name": "Yuval Noah Harari", - "dgraph.type": [ - "author" - ], - "uid": "_:author_3", - "author.book": [ - { - "Book.author": { - "uid": "_:author_3" - }, - "uid": "0x117" - } - ] - } - deletejson: | - [ - { - "author.book": [ - { - "uid": "0x117" - } - ], - "uid": "uid(author_4)" - } - ] - - name: "Add type with multiple Xids fields at deep level when deep node already exist for one existence query" gqlmutation: | @@ -5204,3 +5183,320 @@ "dgraph.type": ["Person"], "uid": "_:Person_1" } + +- + name: "2-level add mutation with nullable @id fields " + explaination: "bookId in Book and PenName in author are @id and nullable field, + we can skip them while doing add mutation. Nested object author doesn't exist, so we + add it and link it to book" + gqlmutation: | + mutation addBook($input: [AddBookInput!]!) { + addBook(input: $input, upsert: false) { + book { + title + } + } + } + gqlvariables: | + { "input": + [ + { + "title": "Sapiens", + "ISBN": "B02", + "publisher": "penguin", + "author": { + "name": "Alice", + "authorId": "A02" + } + } + ] + } + dgquery: |- + query { + Book_1(func: eq(Book.ISBN, "B02")) { + uid + dgraph.type + } + Book_2(func: eq(Book.title, "Sapiens")) { + uid + dgraph.type + } + author_3(func: eq(author.authorId, "A02")) { + uid + dgraph.type + } + } + dgmutations: + - setjson: | + { + "Book.title": "Sapiens", + "Book.ISBN": "B02", + "Book.publisher": "penguin", + "dgraph.type": [ + "Book" + ], + "Book.author": { + "author.authorId":"A02", + "author.book": [ + { + "uid": "_:Book_2" + } + ], + "author.name": "Alice", + "dgraph.type": [ + "author" + ], + "uid": "_:author_3" + }, + "uid": "_:Book_2" + } + +- + name: "2- level add mutation with upsert and nullable @id fields " + explaination: "bookId in @id,penName in author are nullable @id fields and we can skip them. + title,ISBN in Book are @id fields,so also added in set Json, because @id fields will also be updated by upserts. + Both book and author already exist so we just link new author to book and delete old reference from book to author, + if there is any" + gqlmutation: | + mutation addBook($input: [AddBookInput!]!) { + addBook(input: $input, upsert: true) { + book { + title + } + } + } + gqlvariables: | + { "input": + [ + { + "title": "Sapiens", + "ISBN": "B01", + "publisher": "penguin", + "author": { + "name": "Alice", + "authorId": "A01" + } + } + ] + } + dgquery: |- + query { + Book_1(func: eq(Book.ISBN, "B01")) { + uid + dgraph.type + } + Book_2(func: eq(Book.title, "Sapiens")) { + uid + dgraph.type + } + author_3(func: eq(author.authorId, "A01")) { + uid + dgraph.type + } + } + qnametouid: | + { + "Book_2":"0x11", + "author_3": "0x12" + } + dgquerysec: |- + query { + Book_2 as Book_2(func: uid(0x11)) @filter(type(Book)) { + uid + } + var(func: uid(Book_2)) { + author_4 as Book.author @filter(NOT (uid(0x12))) + } + } + dgmutations: + - setjson: | + { + "Book.ISBN": "B01", + "Book.author": { + "author.book": [ + { + "uid": "uid(Book_2)" + } + ], + "uid": "0x12" + }, + "Book.publisher": "penguin", + "Book.title": "Sapiens", + "uid": "uid(Book_2)" + } + deletejson: | + [{ + "author.book": [ + { + "uid": "uid(Book_2)" + } + ], + "uid": "uid(author_4)" + }] + cond: "@if(gt(len(Book_2), 0))" + +- + name: "add mutation with upsert gives error when multiple nodes are found for existence queries" + explaination: "Two different books exist for title and Sapiens @id fields, We can't do upsert mutation " + gqlmutation: | + mutation addBook($input: [AddBookInput!]!) { + addBook(input: $input, upsert: true) { + book { + title + } + } + } + gqlvariables: | + { "input": + [ + { + "title": "Sapiens", + "ISBN": "B01", + "publisher": "penguin" + } + ] + } + dgquery: |- + query { + Book_1(func: eq(Book.ISBN, "B01")) { + uid + dgraph.type + } + Book_2(func: eq(Book.title, "Sapiens")) { + uid + dgraph.type + } + } + qnametouid: | + { + "Book_1":"0x11", + "Book_2": "0x12" + } + error2: + { + "message": "failed to rewrite mutation payload because multiple nodes found + for given xid values, updation not possible" + } + +- + name: "add mutation with upsert at nested level gives error when multiple nodes are found + for existence queries" + explaination: "Two different author exist for penName and authorId @id fields inside author, + We can't link author to both books " + gqlmutation: | + mutation addBook($input: [AddBookInput!]!) { + addBook(input: $input, upsert: true) { + book { + title + } + } + } + gqlvariables: | + { "input": + [ + { + "title": "Sapiens", + "ISBN": "B01", + "publisher": "penguin", + "author": { + "penName": "Alice", + "authorId": "A01" + } + } + ] + } + dgquery: |- + query { + Book_1(func: eq(Book.ISBN, "B01")) { + uid + dgraph.type + } + Book_2(func: eq(Book.title, "Sapiens")) { + uid + dgraph.type + } + author_3(func: eq(author.authorId, "A01")) { + uid + dgraph.type + } + author_4(func: eq(author.penName, "Alice")) { + uid + dgraph.type + } + } + qnametouid: | + { + "author_3":"0x11", + "author_4": "0x12" + } + error2: + { + "message": "failed to rewrite mutation payload because multiple nodes + found for given xid values, updation not possible" + } + +- + name: "No xid present for add mutation with upsert" + explaination: "If none of the xid field is given in upsert mutation then there will be no existence queries, + and it will behave as simple add mutation,i.e. create new node with all the given fields" + gqlmutation: | + mutation addBook($input: [AddBookInput!]!) { + addBook(input: $input, upsert: true) { + book { + title + } + } + } + gqlvariables: | + { "input": + [ + { + "publisher": "penguin" + } + ] + } + dgmutations: + - setjson: | + { + "Book.publisher": "penguin", + "dgraph.type": [ + "Book" + ], + "uid":"_:Book_1" + } + +- + name: "Non-nullable xid should be present in add Mutation for nested field" + explaination: "non-nullable @id field id in comment1 type not provided. As no reference is + provided for comment, we treat it as new node, and return error for missing xid." + gqlmutation: | + mutation addPost1($input: [AddPost1Input!]!) { + addPost1(input: $input, upsert: false) { + post1 { + content + } + } + } + gqlvariables: | + { "input": + [ + { + "id": "P01", + "content":"Intro to GraphQL", + "comments":[{ + "message":"Nice Intro! Love GraphQl" + }] + } + ] + } + dgquery: |- + query { + Post1_1(func: eq(Post1.id, "P01")) { + uid + dgraph.type + } + } + error2: + { + "message": "failed to rewrite mutation payload because field id cannot be empty" + } \ No newline at end of file diff --git a/graphql/resolve/mutation.go b/graphql/resolve/mutation.go index 7e15008322a..a7cb2bd8b0c 100644 --- a/graphql/resolve/mutation.go +++ b/graphql/resolve/mutation.go @@ -433,6 +433,49 @@ func (mr *dgraphResolver) rewriteAndExecute( resolverFailed } } + // for update mutation, if @id field is present in set then we check that + // in filter only one node is selected. if there are multiple nodes selected, + // then it's not possible to update all of them with same value of @id fields. + // In that case we return error + if mutation.MutationType() == schema.UpdateMutation { + inp := mutation.ArgValue(schema.InputArgName).(map[string]interface{}) + setArg := inp["set"] + objSet, okSetArg := setArg.(map[string]interface{}) + if len(objSet) == 0 && okSetArg { + return emptyResult( + schema.GQLWrapf(errors.Errorf("not able to find set args"+ + " in update mutation"), + "mutation %s failed", mutation.Name())), + resolverFailed + } + + mutatedType := mutation.MutatedType() + var xidsPresent bool + if len(objSet) != 0 { + for _, xid := range mutatedType.XIDFields() { + if xidVal, ok := objSet[xid.Name()]; ok && xidVal != nil { + xidsPresent = true + } + } + } + // if @id field is present in set and there are multiple nodes returned from + // upsert query then we return error + if xidsPresent && len(result[mutation.Name()].([]interface{})) > 1 { + if queryAuthSelector(mutatedType) == nil { + return emptyResult( + schema.GQLWrapf(errors.Errorf("only one node is allowed in"+ + " the filter while updating fields with @id directive"), + "mutation %s failed", mutation.Name())), + resolverFailed + } + return emptyResult( + schema.GQLWrapf(errors.Errorf("GraphQL debug: only one node is"+ + " allowed in the filter while updating fields with @id directive"), + "mutation %s failed", mutation.Name())), + resolverFailed + + } + } copyTypeMap(upsert.NewNodes, newNodes) } diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index deb00d61e40..669cfafa6ae 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -1408,8 +1408,9 @@ func rewriteObject( xids := typ.XIDFields() if len(xids) != 0 { - // nonExistingXIDs stores number of uids for which there exist no nodes - var nonExistingXIDs int + // multipleNodesForSameID is true when there are multiple nodes present + // in a result of existence queries + multipleNodesForSameID := gotMultipleExistingNodes(xids, obj, typ, varGen, idExistence) // xidVariables stores the variable names for each XID. var xidVariables []string for _, xid := range xids { @@ -1417,6 +1418,8 @@ func rewriteObject( if xidVal, ok := obj[xid.Name()]; ok && xidVal != nil { xidString, _ = extractVal(xidVal, xid.Name(), xid.Type().Name()) variable = varGen.Next(typ, xid.Name(), xidString, false) + existenceError := x.GqlErrorf("multiple nodes found for given xid values," + + " updation not possible") // If this xid field is inherited from interface and have interface argument set, we also // have existence query for interface to make sure that this xid is unique across all @@ -1429,7 +1432,6 @@ func rewriteObject( // 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. - interfaceTyp, interfaceVar := interfaceVariable(typ, varGen, xid.Name(), xidString) // Get whether node with XID exists or not from existenceQueriesResults @@ -1441,8 +1443,19 @@ func rewriteObject( // We return an error if this is at toplevel. Else, we return the ID reference if // found node is of same type as xid field type. Because that node can be of some other // type in case xidField is inherited from interface. + if atTopLevel { if mutationType == AddWithUpsert { + // returns from here if we got multiple nodes as a result of existence queries. + if multipleNodesForSameID { + if queryAuthSelector(typ) == nil { + retErrors = append(retErrors, existenceError) + } else { + retErrors = append(retErrors, x.GqlErrorf("GraphQL debug: "+existenceError.Error())) + } + + return nil, "", retErrors + } if typUidExist { // This means we are in Add Mutation with upsert: true and node belong to // same type as of the xid field. @@ -1470,7 +1483,7 @@ func rewriteObject( } else { // This error will only be reported in debug mode. err = x.GqlErrorf("GraphQL debug: id %s already exists for field %s"+ - " inside type %s", typ.Name(), xid.Name(), xidString) + " inside type %s", xidString, xid.Name(), typ.Name()) } retErrors = append(retErrors, err) return nil, upsertVar, retErrors @@ -1482,67 +1495,78 @@ func rewriteObject( } } else { + if multipleNodesForSameID { + // returns from here if we got multiple nodes as a result of existence queries. + if queryAuthSelector(typ) == nil { + retErrors = append(retErrors, existenceError) + } else { + retErrors = append(retErrors, x.GqlErrorf("GraphQL debug: "+existenceError.Error())) + } + return nil, "", retErrors + } // As we are not at top level, we return the XID reference. We don't update this node // further. if typUidExist { return asIDReference(ctx, typUid, srcField, srcUID, varGen, mutationType == UpdateWithRemove), upsertVar, nil } + // returns error if xid is present in some other implementing type retErrors = append(retErrors, xidErrorForInterfaceType(typ, xidString, xid, interfaceTyp.Name())) return nil, upsertVar, retErrors } } else { - - // Node with XIDs does not exist. It means this is a new node. - // This node will be created later. - obj = xidMetadata.variableObjMap[variable] xidVariables = append(xidVariables, variable) - // We add a new node only if - // 1. All the xids are present and - // 2. No node exist for any of the xid - if nonExistingXIDs == len(xids)-1 { - exclude := "" - if srcField != nil { - invField := srcField.Inverse() - if invField != nil { - exclude = invField.Name() - } - } - // We replace obj with xidMetadata.variableObjMap[variable] in this case. - // This is done to ensure that the first time we encounter an XID node, we use - // its definition and later times, we just use its reference. - - if err := typ.EnsureNonNulls(obj, exclude); err != nil { - // This object does not contain XID. This is an error. - retErrors = append(retErrors, err) - return nil, upsertVar, retErrors - } - // Set existenceQueryResult to _:variable. This is to make referencing to - // this node later easier. - // Set idExistence for all variables which are referencing this node to - // the blank node _:variable. - // Example: if We have multiple xids inside a type say person, then - // we create a single blank node e.g. _:person1 - // and also two different query variables for xids say person1,person2 and assign - // _:person1 to both of them in idExistence map - // i.e. idExistence[person1]= _:person1 - // idExistence[person2]= _:person1 - for _, xidVariable := range xidVariables { - idExistence[xidVariable] = fmt.Sprintf("_:%s", variable) - } - } - nonExistingXIDs++ + } + } + } + if len(xidVariables) != 0 { + exclude := "" + if srcField != nil { + invField := srcField.Inverse() + if invField != nil { + exclude = invField.Name() } } + // Node with XIDs does not exist. It means this is a new node. + // This node will be created later. + obj = xidMetadata.variableObjMap[xidVariables[0]] + // We replace obj with xidMetadata.variableObjMap[variable] in this case. + // This is done to ensure that the first time we encounter an XID node, we use + // its definition and later times, we just use its reference. + + if err := typ.EnsureNonNulls(obj, exclude); (err != nil) && + !(mutationType == UpdateWithSet && atTopLevel) { + // This object does not contain non nullable XID, returns error. + // We ignore the error for update mutation top level fields. + retErrors = append(retErrors, err) + return nil, upsertVar, retErrors + } + + // Set existenceQueryResult to _:variable. This is to make referencing to + // this node later easier. + // Set idExistence for all variables which are referencing this node to + // the blank node _:variable. + // Example: if We have multiple xids inside a type say person, then + // we create a single blank node e.g. _:person1 + // and also two different query variables for xids say person1,person2 and assign + // _:person1 to both of them in idExistence map + // i.e. idExistence[person1]= _:person1 + // idExistence[person2]= _:person1 + for _, xidVariable := range xidVariables { + idExistence[xidVariable] = fmt.Sprintf("_:%s", variable) + } } + if upsertVar == "" { for _, xid := range xids { + xidType := xid.Type().String() if xidVal, ok := obj[xid.Name()]; ok && xidVal != nil { // This is handled in the for loop above continue - } else if mutationType == Add || mutationType == AddWithUpsert || !atTopLevel { + } else if (mutationType == Add || mutationType == AddWithUpsert || !atTopLevel) && + (xidType == "String!" || xidType == "Int!" || xidType == "Int64!") { // When we reach this stage we are absolutely sure that this is not a reference and is // a new node and one of the XIDs is missing. // There are two possibilities here: @@ -1554,19 +1578,12 @@ func rewriteObject( // 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. + err := errors.Errorf("field %s cannot be empty", xid.Name()) retErrors = append(retErrors, err) return nil, upsertVar, retErrors } } - } else { - // In case this is known to be an Upsert. We delete all entries of XIDs - // from obj. This is done to prevent any XID entries in the json which is returned - // by rewriteObject and ensure that no XID value gets rewritten due to upsert. - for _, xid := range xids { - // To ensure that xid is not added to the output json in case of upsert - delete(obj, xid.Name()) - } } } @@ -1856,7 +1873,7 @@ func existenceQueries( // considered a duplicate of the existing object, then return error. if xidMetadata.isDuplicateXid(atTopLevel, variable, obj, srcField) { - // TODO(GRAPHQL): Add this error for inherited @id field with interface arg. + // TODO(Jatin): Add this error for inherited @id field with interface arg. // Currently we don't return this error for the nested case when // at both root and nested level we have same value of @id fields // which have interface arg set and are inherited from same interface @@ -1872,6 +1889,8 @@ func existenceQueries( // xidMetadata.variableObjMap[variable] = { "id": "1" } // In this case, as obj is the correct definition of the object, we update variableObjMap oldObj := xidMetadata.variableObjMap[variable] + // TODO(Jatin): This condition also needs to change in accordance with multiple xids. + // Also consider the case when @id fields can be nullable. if len(oldObj) == 1 && len(obj) > 1 { // Continue execution to perform dfs in this case. There may be more nodes // in the subtree of this node. @@ -2467,3 +2486,27 @@ func interfaceVariable(typ schema.Type, varGen *VariableGenerator, xidName strin } return nil, "" } + +// This function returns true if there are multiple nodes present +// in a result of existence queries +func gotMultipleExistingNodes(xids []schema.FieldDefinition, obj map[string]interface{}, + typ schema.Type, varGen *VariableGenerator, idExistence map[string]string) bool { + + var existenceNodeUid string + for _, xid := range xids { + if xidVal, ok := obj[xid.Name()]; ok && xidVal != nil { + xidString, _ := extractVal(xidVal, xid.Name(), xid.Type().Name()) + variable := varGen.Next(typ, xid.Name(), xidString, false) + if uid, ok := idExistence[variable]; ok { + if existenceNodeUid == "" { + existenceNodeUid = uid + } else if existenceNodeUid != uid { + return true + } + } + + } + + } + return false +} diff --git a/graphql/resolve/schema.graphql b/graphql/resolve/schema.graphql index fe94fd0cbef..6d8b2a1588d 100644 --- a/graphql/resolve/schema.graphql +++ b/graphql/resolve/schema.graphql @@ -277,6 +277,7 @@ type Person1 { type Comment1 { id: String! @id + commentId: String message: String replies: [Comment1] } @@ -390,15 +391,17 @@ type Node { type Book { id: ID! publisher: String - title: String! @id - ISBN: String! @id + title: String @id + ISBN: String @id + bookId: Int @id author: author - } type author { id: ID! - name: String! + name: String + penName: String @id + authorId: String @id book: [Book] @hasInverse(field: author) } # test for entities resolver diff --git a/graphql/resolve/update_mutation_test.yaml b/graphql/resolve/update_mutation_test.yaml index d4335ceebaa..054b915e091 100644 --- a/graphql/resolve/update_mutation_test.yaml +++ b/graphql/resolve/update_mutation_test.yaml @@ -1979,3 +1979,471 @@ "Author.name": "Alice" } cond: "@if(gt(len(x), 0))" + +- + name: "Updating @id field when given values for @id fields doesn't exists" + explaination: "We are giving two @id fields title and ISBN in set part of update mutation, + and will generate two existence queries for both of them. As none of the @id field is present,we + update the values successfully " + gqlmutation: | + mutation update($patch: UpdateBookInput!) { + updateBook(input: $patch) { + book { + title + ISBN + publisher + } + } + } + gqlvariables: | + { + "patch": { + "filter": { + "or": [ + { + "title": { + "in": "Sapiens" + } + }, + { + "ISBN": { + "in": "2QSAT" + } + } + ] + }, + "set": { + "title": "History of Humans", + "ISBN": "I001", + "publisher": "penguin" + } + } + } + dgquery: |- + query { + Book_1(func: eq(Book.ISBN, "I001")) { + uid + dgraph.type + } + Book_2(func: eq(Book.title, "History of Humans")) { + uid + dgraph.type + } + } + dgquerysec: |- + query { + x as updateBook(func: type(Book)) @filter((eq(Book.title, "Sapiens") OR eq(Book.ISBN, "2QSAT"))) { + uid + } + } + dgmutations: + - setjson: | + { "uid" : "uid(x)", + "Book.ISBN": "I001", + "Book.publisher": "penguin", + "Book.title": "History of Humans" + } + cond: "@if(gt(len(x), 0))" +- + name: "Updating @id field when given value for @id fields exist in some node" + explaination: "We are giving two @id fields title and ISBN in set part of update mutation, + and will generate two existence queries for both of them.As we already have node with title + Sapiens, we will return error in this case " + gqlmutation: | + mutation update($patch: UpdateBookInput!) { + updateBook(input: $patch) { + book { + title + ISBN + publisher + } + } + } + gqlvariables: | + { + "patch": { + "filter": { + "or": [ + { + "title": { + "in": "Sapiens" + } + }, + { + "ISBN": { + "in": "2QSAT" + } + } + ] + }, + "set": { + "title": "History of Humans", + "ISBN": "I001", + "publisher": "penguin" + } + } + } + dgquery: |- + query { + Book_1(func: eq(Book.ISBN, "I001")) { + uid + dgraph.type + } + Book_2(func: eq(Book.title, "History of Humans")) { + uid + dgraph.type + } + } + qnametouid: |- + { + "Book_2": "0x123" + } + error2: + { "message": + "failed to rewrite mutation payload because id History of Humans already exists for field title inside type Book" + } + +- + name: "skipping nullable @id values while Updating link to non-existent nested object" + explaination: "when we update link to nested field, we check if that node already exists or not, + In this case nested object doesn't exists and update mutation create it and link it to root object. + while creating nested object it skip @id nullable fields which don't exists in nested object, in this case + it skips commentId in nested type Comment1" + gqlmutation: | + mutation update($patch: UpdatePost1Input!) { + updatePost1(input: $patch) { + post1 { + id + } + } + } + gqlvariables: | + { + "patch": { + "filter": { + "id": { + "in": "P02" + } + }, + "set": { + "id": "P01", + "content": "intro to graphql", + "comments": { + "id": "C01", + "message": "nice intro!" + } + } + } + } + dgquery: |- + query { + Post1_1(func: eq(Post1.id, "P01")) { + uid + dgraph.type + } + Comment1_2(func: eq(Comment1.id, "C01")) { + uid + dgraph.type + } + } + dgquerysec: |- + query { + x as updatePost1(func: type(Post1)) @filter(eq(Post1.id, "P02")) { + uid + } + } + dgmutations: + - setjson: | + { + "Post1.comments": [ + { + "Comment1.id": "C01", + "Comment1.message": "nice intro!", + "dgraph.type": [ + "Comment1" + ], + "uid": "_:Comment1_2" + } + ], + "Post1.content": "intro to graphql", + "Post1.id": "P01", + "uid": "uid(x)" + } + cond: "@if(gt(len(x), 0))" + +- + name: "Updating link to nested field require all the non-null id's to be present in nested field" + explaination: "when we update link to nested field then we check if that already exist or not, + In this case since @id field is not present in nested field, so we assume it to be a new node. + update mutation tries to create it but failed because non-nullable id field is required to add new + node." + gqlmutation: | + mutation update($patch: UpdatePost1Input!) { + updatePost1(input: $patch) { + post1 { + id + } + } + } + gqlvariables: | + { + "patch": { + "filter": { + "id": { + "in": "P02" + } + }, + "set": { + "id": "P01", + "content": "intro to graphql", + "comments":{ + "message":"nice intro!" + } + } + } + } + dgquery: |- + query { + Post1_1(func: eq(Post1.id, "P01")) { + uid + dgraph.type + } + } + error2: + { "message": + "failed to rewrite mutation payload because field id cannot be empty" + } + +- + name: "Updating inherited @id field with interface arg -1 " + explaination: "For this case we will generate one more existence query for inherited @id field refID which have + interface arg set. No node with given refID exist in same or other implementing type of interface so we will + successfully update node in this case" + gqlmutation: | + mutation update($patch: UpdateLibraryMemberInput!) { + updateLibraryMember(input: $patch) { + libraryMember { + refID + } + } + } + gqlvariables: | + { + "patch": { + "filter": { + "refID": { + "in": "101" + } + }, + "set": { + "refID": "102", + "name": "Alice", + "readHours": "3d2hr" + } + } + } + + dgquery: |- + query { + LibraryMember_1(func: eq(Member.name, "Alice")) { + uid + dgraph.type + } + LibraryMember_2(func: eq(Member.refID, "102")) { + uid + dgraph.type + } + LibraryMember_3(func: eq(Member.refID, "102")) { + uid + dgraph.type + } + } + dgquerysec: |- + query { + x as updateLibraryMember(func: type(LibraryMember)) @filter(eq(Member.refID, "101")) { + uid + } + } + dgmutations: + - setjson: | + { + "LibraryMember.readHours":"3d2hr", + "Member.name":"Alice", + "Member.refID":"102", + "uid":"uid(x)" + } + cond: "@if(gt(len(x), 0))" + +- + name: "Updating inherited @id field with interface arg -2 " + explaination: "For this case we will generate one more existence query for inherited @id field refID. + There already exist node with refID in other implementing type of interface so we will generate error for this case" + gqlmutation: | + mutation update($patch: UpdateLibraryMemberInput!) { + updateLibraryMember(input: $patch) { + libraryMember { + refID + } + } + } + gqlvariables: | + { + "patch": { + "filter": { + "refID": { + "in": "101" + } + }, + "set": { + "refID": "102", + "name": "Alice", + "readHours": "3d2hr" + } + } + } + dgquery: |- + query { + LibraryMember_1(func: eq(Member.name, "Alice")) { + uid + dgraph.type + } + LibraryMember_2(func: eq(Member.refID, "102")) { + uid + dgraph.type + } + LibraryMember_3(func: eq(Member.refID, "102")) { + uid + dgraph.type + } + } + qnametouid: |- + { + "LibraryMember_3": "0x123" + } + error2: + { + "message": "failed to rewrite mutation payload because id 102 already exists for field refID + in some other implementing type of interface Member" + } + +- + name: "Updating link to nested object inheriting @id field with interface argument-1" + explaination: "If nested object have inherited @id field which have interface argument set, and that + field already exist in some other implementing type than we returns error.In below mutation manages + is of type LibraryMember but node with given refID already exist in some other + type than than LibraryMember" + gqlmutation: | + mutation update($patch: UpdateLibraryManagerInput!) { + updateLibraryManager(input: $patch) { + libraryManager { + name + } + } + } + gqlvariables: | + { + "patch": { + "filter": { + "name": { + "in": "Alice" + } + }, + "set": { + "name": "Bob", + "manages": { + "refID":"101" + } + } + } + } + dgquery: |- + query { + LibraryManager_1(func: eq(LibraryManager.name, "Bob")) { + uid + dgraph.type + } + LibraryMember_2(func: eq(Member.refID, "101")) { + uid + dgraph.type + } + LibraryMember_3(func: eq(Member.refID, "101")) { + uid + dgraph.type + } + } + qnametouid: |- + { + "LibraryMember_3": "0x123" + } + error2: + { + "message": "failed to rewrite mutation payload because id 101 already exists for field refID + in some other implementing type of interface Member" + } + +- + name: "Updating link to nested object inheriting @id field with interface argument-2" + explaination: "In below mutation manages is of type LibraryMember and node of type LibraryMember already + existed with given refID, so we link that correctly" + gqlmutation: | + mutation update($patch: UpdateLibraryManagerInput!) { + updateLibraryManager(input: $patch) { + libraryManager { + name + } + } + } + gqlvariables: | + { + "patch": { + "filter": { + "name": { + "in": "Alice" + } + }, + "set": { + "name": "Bob", + "manages": { + "refID":"101" + } + } + } + } + dgquery: |- + query { + LibraryManager_1(func: eq(LibraryManager.name, "Bob")) { + uid + dgraph.type + } + LibraryMember_2(func: eq(Member.refID, "101")) { + uid + dgraph.type + } + LibraryMember_3(func: eq(Member.refID, "101")) { + uid + dgraph.type + } + } + qnametouid: |- + { + "LibraryMember_2": "0x123", + "LibraryMember_3": "0x124" + } + dgquerysec: |- + query { + x as updateLibraryManager(func: type(LibraryManager)) @filter(eq(LibraryManager.name, "Alice")) { + uid + } + } + dgmutations: + - setjson: | + { + "LibraryManager.manages": [ + { + "uid": "0x123" + } + ], + "LibraryManager.name": "Bob", + "uid": "uid(x)" + } + cond: "@if(gt(len(x), 0))" diff --git a/graphql/resolve/validate_mutation_test.yaml b/graphql/resolve/validate_mutation_test.yaml index 713ce8dbce0..828dec13f5e 100644 --- a/graphql/resolve/validate_mutation_test.yaml +++ b/graphql/resolve/validate_mutation_test.yaml @@ -18,9 +18,9 @@ explanation: "Add mutation expects an array instead of an object" validationerror: { "message": - "input:2: Variable type provided AddAuthorInput! is incompatible with expected - type [AddAuthorInput!]!\ninput:2: Variable \"$auth\" of type \"AddAuthorInput!\" - used in position expecting type \"[AddAuthorInput!]!\".\n" } + "input:2: Variable \"$auth\" of type \"AddAuthorInput!\" used in position expecting type + \"[AddAuthorInput!]!\".\ninput:2: Variable type provided AddAuthorInput! is incompatible + with expected type [AddAuthorInput!]!\n" } - diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index 185611e50be..7927f547e64 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -1291,8 +1291,8 @@ func addPatchType(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[ nonIDFields := getNonIDFields(schema, defn, providesTypeMap) if len(nonIDFields) == 0 { - // The user might just have an external id field and nothing else. We don't generate patch - // type in that case. + // The user might just have an predicate with reverse edge id field and nothing else. + // We don't generate patch type in that case. return } @@ -1827,9 +1827,6 @@ func addUpdatePayloadType(schema *ast.Schema, defn *ast.Definition, providesType return } - // This covers the case where the Type only had one field (which had @id directive). - // Since we don't allow updating the field with @id directive we don't need to generate any - // update payload. if _, ok := schema.Types[defn.Name+"Patch"]; !ok { return } @@ -2236,7 +2233,7 @@ func createField(schema *ast.Schema, fld *ast.FieldDefinition) *ast.FieldDefinit func getNonIDFields(schema *ast.Schema, defn *ast.Definition, providesTypeMap map[string]bool) ast.FieldList { fldList := make([]*ast.FieldDefinition, 0) for _, fld := range defn.Fields { - if isIDField(defn, fld) || hasIDDirective(fld) { + if isIDField(defn, fld) { continue } diff --git a/graphql/schema/gqlschema_test.yml b/graphql/schema/gqlschema_test.yml index b31b6e86705..64f918f05e7 100644 --- a/graphql/schema/gqlschema_test.yml +++ b/graphql/schema/gqlschema_test.yml @@ -239,7 +239,7 @@ invalid_schemas: errlist: [ { "message": "Type Z; Field f: Field f is of type U, but @hasInverse directive only applies to fields with object types.", "locations": [{"line":9, "column":3}]}, { "message": "Type Z; Field f: has the @search directive but fields of type U can't have the @search directive.", "locations": [{"line":9, "column":34}]}, - { "message": "Type Z; Field f: with @id directive must be of type String!, Int! or Int64!, not U", "locations": [{"line":9, "column":42}]} + { "message": "Type Z; Field f: with @id directive must be of type String, Int or Int64, not U", "locations": [{"line":9, "column":42}]} ] - @@ -622,7 +622,7 @@ invalid_schemas: f1: [String] @id } errlist: [ - {"message": "Type X; Field f1: with @id directive must be of type String!, Int! or Int64!, not [String]", + {"message": "Type X; Field f1: with @id directive must be of type String, Int or Int64, not [String]", "locations":[{"line":2, "column":17}]} ] @@ -633,18 +633,7 @@ invalid_schemas: f1: Float! @id } errlist: [ - {"message": "Type X; Field f1: with @id directive must be of type String!, Int! or Int64!, not Float!", - "locations":[{"line":2, "column":15}]} - ] - - - - name: "Field with @id directive should be mandatory" - input: | - type X { - f1: String @id - } - errlist: [ - {"message": "Type X; Field f1: with @id directive must be of type String!, Int! or Int64!, not String", + {"message": "Type X; Field f1: with @id directive must be of type String, Int or Int64, not Float!", "locations":[{"line":2, "column":15}]} ] diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index 1647911ba65..78518d11821 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -2110,9 +2110,9 @@ func idValidation(sch *ast.Schema, field *ast.FieldDefinition, dir *ast.Directive, secrets map[string]x.Sensitive) gqlerror.List { - if field.Type.String() == "String!" || - field.Type.String() == "Int!" || - field.Type.String() == "Int64!" { + if field.Type.NamedType == "String" || + field.Type.NamedType == "Int" || + field.Type.NamedType == "Int64" { var inherited bool for _, implements := range sch.Implements[typ.Name] { @@ -2130,7 +2130,7 @@ func idValidation(sch *ast.Schema, } return []*gqlerror.Error{gqlerror.ErrorPosf( dir.Position, - "Type %s; Field %s: with @id directive must be of type String!, Int! or Int64!, not %s", + "Type %s; Field %s: with @id directive must be of type String, Int or Int64, not %s", typ.Name, field.Name, field.Type.String())} } diff --git a/graphql/schema/testdata/apolloservice/output/auth-directive.graphql b/graphql/schema/testdata/apolloservice/output/auth-directive.graphql index 1d3488c0b27..d11d8dd6f8b 100644 --- a/graphql/schema/testdata/apolloservice/output/auth-directive.graphql +++ b/graphql/schema/testdata/apolloservice/output/auth-directive.graphql @@ -439,6 +439,7 @@ input UserOrder { } input UserPatch { + username: String todos: [TodoRef] } diff --git a/graphql/schema/testdata/apolloservice/output/extended-types.graphql b/graphql/schema/testdata/apolloservice/output/extended-types.graphql index 31024cd9323..4c2a6ab110b 100644 --- a/graphql/schema/testdata/apolloservice/output/extended-types.graphql +++ b/graphql/schema/testdata/apolloservice/output/extended-types.graphql @@ -494,6 +494,7 @@ input ProductOrder { } input ProductPatch { + upc: String inStock: Boolean shippingEstimate: Float } diff --git a/graphql/schema/testdata/apolloservice/output/single-extended-type.graphql b/graphql/schema/testdata/apolloservice/output/single-extended-type.graphql index 0704e8fd6f3..94acbbcc7a3 100644 --- a/graphql/schema/testdata/apolloservice/output/single-extended-type.graphql +++ b/graphql/schema/testdata/apolloservice/output/single-extended-type.graphql @@ -322,6 +322,7 @@ input ProductOrder { } input ProductPatch { + id: String name: String } diff --git a/graphql/schema/testdata/schemagen/input/custom-dql-query-with-subscription.graphql b/graphql/schema/testdata/schemagen/input/custom-dql-query-with-subscription.graphql index de91499335b..9e780434213 100644 --- a/graphql/schema/testdata/schemagen/input/custom-dql-query-with-subscription.graphql +++ b/graphql/schema/testdata/schemagen/input/custom-dql-query-with-subscription.graphql @@ -5,12 +5,12 @@ type Tweets { timestamp: DateTime! @search } type User { - screen_name: String! @id + screenName: String! @id followers: Int @search tweets: [Tweets] @hasInverse(field: author) } type UserTweetCount @remote { - screen_name: String + screenName: String tweetCount: Int } @@ -18,7 +18,7 @@ type Query { queryUserTweetCounts : [UserTweetCount] @withSubscription @custom(dql: """ query { queryUserTweetCounts(func: type(User)) { - screen_name: User.screen_name + screenName: User.screenName tweetCount: count(User.tweets) } } diff --git a/graphql/schema/testdata/schemagen/output/apollo-federation.graphql b/graphql/schema/testdata/schemagen/output/apollo-federation.graphql index 91642aebb01..6e2ce105791 100644 --- a/graphql/schema/testdata/schemagen/output/apollo-federation.graphql +++ b/graphql/schema/testdata/schemagen/output/apollo-federation.graphql @@ -571,6 +571,7 @@ input CountryOrder { } input CountryPatch { + code: String name: String } @@ -720,6 +721,7 @@ input UserOrder { } input UserPatch { + name: String age: Int reviews: [ReviewsRef] } diff --git a/graphql/schema/testdata/schemagen/output/authorization.graphql b/graphql/schema/testdata/schemagen/output/authorization.graphql index 8d8a2da3d46..b04bf479f80 100644 --- a/graphql/schema/testdata/schemagen/output/authorization.graphql +++ b/graphql/schema/testdata/schemagen/output/authorization.graphql @@ -452,6 +452,7 @@ input UserOrder { } input UserPatch { + username: String todos: [TodoRef] } diff --git a/graphql/schema/testdata/schemagen/output/custom-dql-query-with-subscription.graphql b/graphql/schema/testdata/schemagen/output/custom-dql-query-with-subscription.graphql index a6c89246ef5..99d4d850e10 100755 --- a/graphql/schema/testdata/schemagen/output/custom-dql-query-with-subscription.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-dql-query-with-subscription.graphql @@ -10,14 +10,14 @@ type Tweets { } type User { - screen_name: String! @id + screenName: String! @id followers: Int @search tweets(filter: TweetsFilter, order: TweetsOrder, first: Int, offset: Int): [Tweets] @hasInverse(field: author) tweetsAggregate(filter: TweetsFilter): TweetsAggregateResult } type UserTweetCount @remote { - screen_name: String + screenName: String tweetCount: Int } @@ -329,8 +329,8 @@ type UpdateUserPayload { type UserAggregateResult { count: Int - screen_nameMin: String - screen_nameMax: String + screenNameMin: String + screenNameMax: String followersMin: Int followersMax: Int followersSum: Int @@ -353,13 +353,13 @@ enum TweetsOrderable { } enum UserHasFilter { - screen_name + screenName followers tweets } enum UserOrderable { - screen_name + screenName followers } @@ -374,7 +374,7 @@ input AddTweetsInput { } input AddUserInput { - screen_name: String! + screenName: String! followers: Int tweets: [TweetsRef] } @@ -421,7 +421,7 @@ input UpdateUserInput { } input UserFilter { - screen_name: StringHashFilter + screenName: StringHashFilter followers: IntFilter has: [UserHasFilter] and: [UserFilter] @@ -436,12 +436,13 @@ input UserOrder { } input UserPatch { + screenName: String followers: Int tweets: [TweetsRef] } input UserRef { - screen_name: String + screenName: String followers: Int tweets: [TweetsRef] } @@ -451,11 +452,11 @@ input UserRef { ####################### type Query { - queryUserTweetCounts: [UserTweetCount] @withSubscription @custom(dql: "query {\n queryUserTweetCounts(func: type(User)) {\n screen_name: User.screen_name\n tweetCount: count(User.tweets)\n }\n}") + queryUserTweetCounts: [UserTweetCount] @withSubscription @custom(dql: "query {\n queryUserTweetCounts(func: type(User)) {\n screenName: User.screenName\n tweetCount: count(User.tweets)\n }\n}") getTweets(id: ID!): Tweets queryTweets(filter: TweetsFilter, order: TweetsOrder, first: Int, offset: Int): [Tweets] aggregateTweets(filter: TweetsFilter): TweetsAggregateResult - getUser(screen_name: String!): User + getUser(screenName: String!): User queryUser(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] aggregateUser(filter: UserFilter): UserAggregateResult } @@ -478,5 +479,5 @@ type Mutation { ####################### type Subscription { - queryUserTweetCounts: [UserTweetCount] @withSubscription @custom(dql: "query {\n queryUserTweetCounts(func: type(User)) {\n screen_name: User.screen_name\n tweetCount: count(User.tweets)\n }\n}") + queryUserTweetCounts: [UserTweetCount] @withSubscription @custom(dql: "query {\n queryUserTweetCounts(func: type(User)) {\n screenName: User.screenName\n tweetCount: count(User.tweets)\n }\n}") } diff --git a/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql b/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql index 47653c74222..c352bb70d87 100755 --- a/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql @@ -345,6 +345,11 @@ type UpdateAuthorPayload { numUids: Int } +type UpdateGenrePayload { + genre(filter: GenreFilter, order: GenreOrder, first: Int, offset: Int): [Genre] + numUids: Int +} + type UpdatePostPayload { post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] numUids: Int @@ -419,6 +424,7 @@ input AuthorOrder { } input AuthorPatch { + name: String pen_name: String posts: [PostRef] } @@ -444,6 +450,10 @@ input GenreOrder { then: GenreOrder } +input GenrePatch { + name: String +} + input GenreRef { name: String! } @@ -487,6 +497,12 @@ input UpdateAuthorInput { remove: AuthorPatch } +input UpdateGenreInput { + filter: GenreFilter! + set: GenrePatch + remove: GenrePatch +} + input UpdatePostInput { filter: PostFilter! set: PostPatch @@ -521,6 +537,7 @@ type Mutation { updateAuthor(input: UpdateAuthorInput!): UpdateAuthorPayload deleteAuthor(filter: AuthorFilter!): DeleteAuthorPayload addGenre(input: [AddGenreInput!]!, upsert: Boolean): AddGenrePayload + updateGenre(input: UpdateGenreInput!): UpdateGenrePayload deleteGenre(filter: GenreFilter!): DeleteGenrePayload } diff --git a/graphql/schema/testdata/schemagen/output/field-with-multiple-@id-fields.graphql b/graphql/schema/testdata/schemagen/output/field-with-multiple-@id-fields.graphql index d8b93ef7b08..13aa2507611 100755 --- a/graphql/schema/testdata/schemagen/output/field-with-multiple-@id-fields.graphql +++ b/graphql/schema/testdata/schemagen/output/field-with-multiple-@id-fields.graphql @@ -345,6 +345,11 @@ type UpdateAuthorPayload { numUids: Int } +type UpdateGenrePayload { + genre(filter: GenreFilter, order: GenreOrder, first: Int, offset: Int): [Genre] + numUids: Int +} + type UpdatePostPayload { post(filter: PostFilter, order: PostOrder, first: Int, offset: Int): [Post] numUids: Int @@ -420,6 +425,8 @@ input AuthorOrder { } input AuthorPatch { + name: String + pen_name: String posts: [PostRef] } @@ -444,6 +451,10 @@ input GenreOrder { then: GenreOrder } +input GenrePatch { + name: String +} + input GenreRef { name: String! } @@ -487,6 +498,12 @@ input UpdateAuthorInput { remove: AuthorPatch } +input UpdateGenreInput { + filter: GenreFilter! + set: GenrePatch + remove: GenrePatch +} + input UpdatePostInput { filter: PostFilter! set: PostPatch @@ -521,6 +538,7 @@ type Mutation { updateAuthor(input: UpdateAuthorInput!): UpdateAuthorPayload deleteAuthor(filter: AuthorFilter!): DeleteAuthorPayload addGenre(input: [AddGenreInput!]!, upsert: Boolean): AddGenrePayload + updateGenre(input: UpdateGenreInput!): UpdateGenrePayload deleteGenre(filter: GenreFilter!): DeleteGenrePayload } diff --git a/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql b/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql index db2e3d3be50..b69bab2269d 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql @@ -342,6 +342,11 @@ type UpdateBookPayload { numUids: Int } +type UpdateLibraryItemPayload { + libraryItem(filter: LibraryItemFilter, order: LibraryItemOrder, first: Int, offset: Int): [LibraryItem] + numUids: Int +} + type UpdateLibraryPayload { library(filter: LibraryFilter, first: Int, offset: Int): [Library] numUids: Int @@ -410,6 +415,8 @@ input BookOrder { } input BookPatch { + refID: String + itemID: String title: String author: String } @@ -443,6 +450,11 @@ input LibraryItemOrder { then: LibraryItemOrder } +input LibraryItemPatch { + refID: String + itemID: String +} + input LibraryItemRef { refID: String! } @@ -467,6 +479,12 @@ input UpdateLibraryInput { remove: LibraryPatch } +input UpdateLibraryItemInput { + filter: LibraryItemFilter! + set: LibraryItemPatch + remove: LibraryItemPatch +} + ####################### # Generated Query ####################### @@ -487,6 +505,7 @@ type Query { ####################### type Mutation { + updateLibraryItem(input: UpdateLibraryItemInput!): UpdateLibraryItemPayload deleteLibraryItem(filter: LibraryItemFilter!): DeleteLibraryItemPayload addBook(input: [AddBookInput!]!, upsert: Boolean): AddBookPayload updateBook(input: UpdateBookInput!): UpdateBookPayload diff --git a/graphql/schema/testdata/schemagen/output/language-tags.graphql b/graphql/schema/testdata/schemagen/output/language-tags.graphql index 09bbce78f52..75d0f6daa9a 100755 --- a/graphql/schema/testdata/schemagen/output/language-tags.graphql +++ b/graphql/schema/testdata/schemagen/output/language-tags.graphql @@ -447,6 +447,7 @@ input PersonPatch { f1Hi: String f2: String f3: String + name: String nameHi: String nameEn: String address: String diff --git a/graphql/schema/testdata/schemagen/output/password-type.graphql b/graphql/schema/testdata/schemagen/output/password-type.graphql index cba6c170459..7bfe0bf4007 100755 --- a/graphql/schema/testdata/schemagen/output/password-type.graphql +++ b/graphql/schema/testdata/schemagen/output/password-type.graphql @@ -336,6 +336,7 @@ input AuthorOrder { } input AuthorPatch { + name: String token: String pwd: String }