diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index 780387212e2..25fe4fad1ae 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -392,6 +392,7 @@ func RunAll(t *testing.T) { t.Run("fragment in mutation", fragmentInMutation) t.Run("fragment in query", fragmentInQuery) t.Run("fragment in query on Interface", fragmentInQueryOnInterface) + t.Run("fragment in query on union", fragmentInQueryOnUnion) t.Run("fragment in query on Object", fragmentInQueryOnObject) // lambda tests diff --git a/graphql/e2e/common/fragment.go b/graphql/e2e/common/fragment.go index 713bd805703..bb5b109a55f 100644 --- a/graphql/e2e/common/fragment.go +++ b/graphql/e2e/common/fragment.go @@ -21,6 +21,8 @@ import ( "fmt" "testing" + "github.com/dgraph-io/dgraph/testutil" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" @@ -383,6 +385,121 @@ func fragmentInQueryOnInterface(t *testing.T) { deleteThingTwo(t, thingTwoId) } +func fragmentInQueryOnUnion(t *testing.T) { + newStarship := addStarship(t) + humanID := addHuman(t, newStarship.ID) + homeId, dogId, parrotId, plantId := addHome(t, humanID) + + queryHomeParams := &GraphQLParams{ + Query: `query { + queryHome { + members { + __typename + ... on Animal { + category + } + ... on Dog { + id + breed + } + ... on Parrot { + repeatsWords + } + ... on Employee { + ename + } + ... on Character { + id + } + ... on Human { + name + } + ... on Plant { + id + } + } + } + qh: queryHome { + members { + ... on Animal { + __typename + } + ... on Dog { + breed + } + ... on Human { + name + } + ... on Plant { + breed + } + } + } + } + `, + } + + gqlResponse := queryHomeParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + queryHomeExpected := fmt.Sprintf(` + { + "queryHome": [ + { + "members": [ + { + "__typename": "Human", + "ename": "Han_employee", + "id": "%s", + "name": "Han" + }, + { + "__typename": "Dog", + "category": "Mammal", + "id": "%s", + "breed": "German Shephard" + }, + { + "__typename": "Parrot", + "category": "Bird", + "repeatsWords": [ + "Good Morning!", + "squawk" + ] + }, + { + "__typename": "Plant", + "id": "%s" + } + ] + } + ], + "qh": [ + { + "members": [ + { + "name": "Han" + }, + { + "__typename": "Dog", + "breed": "German Shephard" + }, + { + "__typename": "Parrot" + }, + { + "breed": "Flower" + } + ] + } + ] + }`, humanID, dogId, plantId) + testutil.CompareJSON(t, queryHomeExpected, string(gqlResponse.Data)) + + cleanupStarwars(t, newStarship.ID, humanID, "") + deleteHome(t, homeId, dogId, parrotId, plantId) +} + func fragmentInQueryOnObject(t *testing.T) { newStarship := addStarship(t) humanID := addHuman(t, newStarship.ID) diff --git a/graphql/e2e/common/mutation.go b/graphql/e2e/common/mutation.go index 3339bf0a33a..348b4a35ccc 100644 --- a/graphql/e2e/common/mutation.go +++ b/graphql/e2e/common/mutation.go @@ -2376,7 +2376,7 @@ func addDroid(t *testing.T) string { } func addThingOne(t *testing.T) string { - addDroidParams := &GraphQLParams{ + addThingOneParams := &GraphQLParams{ Query: `mutation addThingOne($input: AddThingOneInput!) { addThingOne(input: [$input]) { thingOne { @@ -2391,7 +2391,7 @@ func addThingOne(t *testing.T) string { }}, } - gqlResponse := addDroidParams.ExecuteAsPost(t, GraphqlURL) + gqlResponse := addThingOneParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -2409,7 +2409,7 @@ func addThingOne(t *testing.T) string { } func addThingTwo(t *testing.T) string { - addDroidParams := &GraphQLParams{ + addThingTwoParams := &GraphQLParams{ Query: `mutation addThingTwo($input: AddThingTwoInput!) { addThingTwo(input: [$input]) { thingTwo { @@ -2424,7 +2424,7 @@ func addThingTwo(t *testing.T) string { }}, } - gqlResponse := addDroidParams.ExecuteAsPost(t, GraphqlURL) + gqlResponse := addThingTwoParams.ExecuteAsPost(t, GraphqlURL) RequireNoGQLErrors(t, gqlResponse) var result struct { @@ -2441,6 +2441,111 @@ func addThingTwo(t *testing.T) string { return result.AddThingTwo.ThingTwo[0].ID } +func addHome(t *testing.T, humanId string) (string, string, string, string) { + addHomeParams := &GraphQLParams{ + Query: `mutation addHome($input: AddHomeInput!) { + addHome(input: [$input]) { + home { + id + members { + __typename + ... on Animal { + id + } + ... on Human { + id + } + ... on Plant { + id + } + } + } + } + }`, + Variables: map[string]interface{}{ + "input": map[string]interface{}{ + "address": "Avenger Street", + "members": []interface{}{ + map[string]interface{}{ + "dogRef": map[string]interface{}{ + "category": "Mammal", + "breed": "German Shephard", + }, + }, + map[string]interface{}{ + "parrotRef": map[string]interface{}{ + "category": "Bird", + "repeatsWords": []interface{}{ + "squawk", + "Good Morning!", + }, + }, + }, + map[string]interface{}{ + "humanRef": map[string]interface{}{ + "id": humanId, + }, + }, + map[string]interface{}{ + "plantRef": map[string]interface{}{ + "breed": "Flower", + }, + }, + }, + "favouriteMember": map[string]interface{}{ + "humanRef": map[string]interface{}{ + "id": humanId, + }, + }, + }, + }, + } + + gqlResponse := addHomeParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + var result struct { + AddHome struct { + Home []struct { + ID string + Members []struct { + Typename string `json:"__typename"` + ID string + } + } + } + } + err := json.Unmarshal([]byte(gqlResponse.Data), &result) + require.NoError(t, err) + + homeId := result.AddHome.Home[0].ID + requireUID(t, homeId) + + var dogId, parrotId, plantId string + for _, member := range result.AddHome.Home[0].Members { + switch member.Typename { + case "Dog": + dogId = member.ID + case "Parrot": + parrotId = member.ID + case "Plant": + plantId = member.ID + } + } + return homeId, dogId, parrotId, plantId +} + +func deleteHome(t *testing.T, homeId, dogId, parrotId, plantId string) { + homeFilter := map[string]interface{}{"id": []string{homeId}} + deleteGqlType(t, "Home", homeFilter, 1, nil) + dogFilter := map[string]interface{}{"id": []string{dogId}} + deleteGqlType(t, "Dog", dogFilter, 1, nil) + parrotFilter := map[string]interface{}{"id": []string{parrotId}} + deleteGqlType(t, "Parrot", parrotFilter, 1, nil) + plantFilter := map[string]interface{}{"id": []string{plantId}} + deleteGqlType(t, "Plant", plantFilter, 1, nil) +} + func deleteThingOne(t *testing.T, thingOneId string) { thingOneFilter := map[string]interface{}{"id": []string{thingOneId}} deleteGqlType(t, "ThingOne", thingOneFilter, 1, nil) diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index 9cbd7bd505f..54457a5a939 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -194,6 +194,58 @@ type Person1 { friends: [Person1] @hasInverse(field: friends) } +# union testing - start +enum AnimalCategory { + Fish + Amphibian + Reptile + Bird + Mammal + InVertebrate +} + +interface Animal { + id: ID! + category: AnimalCategory @search +} + +type Dog implements Animal { + breed: String @search +} + +type Parrot implements Animal { + repeatsWords: [String] +} + +type Cheetah implements Animal { + speed: Float +} + +""" +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 +} + +union HomeMember = Dog | Parrot | Human | Plant + +type Zoo { + id: ID! + animals: [Animal] + city: String +} + +type Home { + id: ID! + address: String + members: [HomeMember] + favouriteMember: HomeMember +} +# union testing - end + type Query { authorsByName(name: String!): [Author] @lambda } diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index 89040bba11a..52f2cc5c267 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -388,6 +388,57 @@ { "predicate": "职业", "type": "string" + }, + { + "predicate": "Animal.category", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ] + }, + { + "predicate": "Dog.breed", + "type": "string", + "index": true, + "tokenizer": [ + "term" + ] + }, + { + "predicate": "Parrot.repeatsWords", + "type": "string", + "list": true + }, + { + "predicate": "Cheetah.speed", + "type": "float" + }, + { + "predicate": "Plant.breed", + "type": "string" + }, + { + "predicate": "Zoo.animals", + "type": "uid", + "list": true + }, + { + "predicate": "Zoo.city", + "type": "string" + }, + { + "predicate": "Home.address", + "type": "string" + }, + { + "predicate": "Home.members", + "type": "uid", + "list": true + }, + { + "predicate": "Home.favouriteMember", + "type": "uid" } ], "types": [ @@ -744,6 +795,80 @@ } ], "name": "test.dgraph.employee.en" + }, + { + "fields": [ + { + "name": "Animal.category" + } + ], + "name": "Animal" + }, + { + "fields": [ + { + "name": "Animal.category" + }, + { + "name": "Dog.breed" + } + ], + "name": "Dog" + }, + { + "fields": [ + { + "name": "Animal.category" + }, + { + "name": "Parrot.repeatsWords" + } + ], + "name": "Parrot" + }, + { + "fields": [ + { + "name": "Animal.category" + }, + { + "name": "Cheetah.speed" + } + ], + "name": "Cheetah" + }, + { + "fields": [ + { + "name": "Plant.breed" + } + ], + "name": "Plant" + }, + { + "fields": [ + { + "name": "Zoo.animals" + }, + { + "name": "Zoo.city" + } + ], + "name": "Zoo" + }, + { + "fields": [ + { + "name": "Home.address" + }, + { + "name": "Home.members" + }, + { + "name": "Home.favouriteMember" + } + ], + "name": "Home" } ] } \ No newline at end of file diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index 2ca96f33f99..f0972a6656c 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -195,6 +195,58 @@ type Person1 { friends: [Person1] @hasInverse(field: friends) } +# union testing - start +enum AnimalCategory { + Fish + Amphibian + Reptile + Bird + Mammal + InVertebrate +} + +interface Animal { + id: ID! + category: AnimalCategory @search +} + +type Dog implements Animal { + breed: String @search +} + +type Parrot implements Animal { + repeatsWords: [String] +} + +type Cheetah implements Animal { + speed: Float +} + +""" +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 +} + +union HomeMember = Dog | Parrot | Human | Plant + +type Zoo { + id: ID! + animals: [Animal] + city: String +} + +type Home { + id: ID! + address: String + members: [HomeMember] + favouriteMember: HomeMember +} +# union testing - end + type Query { authorsByName(name: String!): [Author] @lambda } diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index 2a1d2dacb9f..a87a0ed9f40 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -335,6 +335,57 @@ "predicate": "User.password", "type": "password" }, + { + "predicate": "Animal.category", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ] + }, + { + "predicate": "Dog.breed", + "type": "string", + "index": true, + "tokenizer": [ + "term" + ] + }, + { + "predicate": "Parrot.repeatsWords", + "type": "string", + "list": true + }, + { + "predicate": "Cheetah.speed", + "type": "float" + }, + { + "predicate": "Plant.breed", + "type": "string" + }, + { + "predicate": "Zoo.animals", + "type": "uid", + "list": true + }, + { + "predicate": "Zoo.city", + "type": "string" + }, + { + "predicate": "Home.address", + "type": "string" + }, + { + "predicate": "Home.members", + "type": "uid", + "list": true + }, + { + "predicate": "Home.favouriteMember", + "type": "uid" + }, { "predicate": "dgraph.cors", "type": "string", @@ -712,6 +763,80 @@ ], "name": "User" }, + { + "fields": [ + { + "name": "Animal.category" + } + ], + "name": "Animal" + }, + { + "fields": [ + { + "name": "Animal.category" + }, + { + "name": "Dog.breed" + } + ], + "name": "Dog" + }, + { + "fields": [ + { + "name": "Animal.category" + }, + { + "name": "Parrot.repeatsWords" + } + ], + "name": "Parrot" + }, + { + "fields": [ + { + "name": "Animal.category" + }, + { + "name": "Cheetah.speed" + } + ], + "name": "Cheetah" + }, + { + "fields": [ + { + "name": "Plant.breed" + } + ], + "name": "Plant" + }, + { + "fields": [ + { + "name": "Zoo.animals" + }, + { + "name": "Zoo.city" + } + ], + "name": "Zoo" + }, + { + "fields": [ + { + "name": "Home.address" + }, + { + "name": "Home.members" + }, + { + "name": "Home.favouriteMember" + } + ], + "name": "Home" + }, { "fields": [ { diff --git a/graphql/e2e/schema/generatedSchema.graphql b/graphql/e2e/schema/generatedSchema.graphql index 2fda70537b0..746c38bd513 100644 --- a/graphql/e2e/schema/generatedSchema.graphql +++ b/graphql/e2e/schema/generatedSchema.graphql @@ -104,7 +104,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/resolve/add_mutation_test.yaml b/graphql/resolve/add_mutation_test.yaml index b44287d53df..df93ab1a32b 100644 --- a/graphql/resolve/add_mutation_test.yaml +++ b/graphql/resolve/add_mutation_test.yaml @@ -2563,3 +2563,100 @@ "uid": "_:Person1" } +- + name: "Add mutation with union" + gqlmutation: | + mutation($input: [AddHomeInput!]!) { + addHome(input: $input) { + home { + address + members { + ... on Dog { + breed + } + } + } + } + } + gqlvariables: | + { + "input": [ + { + "address": "United Street", + "members": [ + { "dogRef": { "category": "Mammal", "breed": "German Shephard"} }, + { "parrotRef": { "category": "Bird", "repeatsWords": ["squawk"]} }, + { "humanRef": { "name": "Han Solo", "ename": "Han_emp"} } + ], + "favouriteMember": { "parrotRef": { "id": "0x123"} } + } + ] + } + dgquery: |- + query { + Parrot2 as Parrot2(func: uid(0x123)) @filter(type(Parrot)) { + uid + } + } + dgmutations: + - setjson: | + { + "Home.address": "United Street", + "Home.favouriteMember": { + "uid": "0x123" + }, + "Home.members": [{ + "Animal.category": "Mammal", + "Dog.breed": "German Shephard", + "dgraph.type": ["Dog", "Animal"], + "uid": "_:Dog3" + }, { + "Animal.category": "Bird", + "Parrot.repeatsWords": ["squawk"], + "dgraph.type": ["Parrot", "Animal"], + "uid": "_:Parrot4" + }, { + "Character.name": "Han Solo", + "Employee.ename": "Han_emp", + "dgraph.type": ["Human", "Character", "Employee"], + "uid": "_:Human5" + }], + "dgraph.type": ["Home"], + "uid": "_:Home1" + } + cond: "@if(eq(len(Parrot2), 1))" + +- + name: "Add mutation with union - invalid input" + gqlmutation: | + mutation($input: [AddHomeInput!]!) { + addHome(input: $input) { + home { + address + members { + ... on Dog { + breed + } + } + } + } + } + gqlvariables: | + { + "input": [ + { + "address": "United Street", + "members": [ + { "dogRef": { "category": "Mammal"}, "parrotRef": { "category": "Bird"} }, + { "parrotRef": { "category": "Bird", "repeatsWords": ["squawk"]} }, + { "humanRef": { "name": "Han Solo", "ename": "Han_emp"} } + ], + "favouriteMember": { } + } + ] + } + explanation: "The add mutation should not be allowed since the union input is invalid" + error: + message: |- + failed to rewrite mutation payload because value for field `favouriteMember` in type `Home` must have exactly one child, found 0 children + failed to rewrite mutation payload because value for field `members` in type `Home` index `0` must have exactly one child, found 2 children diff --git a/graphql/resolve/mutation_rewriter.go b/graphql/resolve/mutation_rewriter.go index dfd2a3e32b8..b87d629e311 100644 --- a/graphql/resolve/mutation_rewriter.go +++ b/graphql/resolve/mutation_rewriter.go @@ -663,7 +663,7 @@ func RewriteUpsertQueryFromMutation(m schema.Mutation, authRw *authRewriter) *gq addTypeFunc(dgQuery, m.MutatedType().DgraphName()) } - addFilter(dgQuery, m.MutatedType(), filter) + _ = addFilter(dgQuery, m.MutatedType(), filter) dgQuery = authRw.addAuthQueries(m.MutatedType(), dgQuery, rbac) @@ -1204,14 +1204,10 @@ func rewriteObject( switch val := val.(type) { case map[string]interface{}: - // This field is another GraphQL object, which could either be linking to an - // existing node by it's ID - // { "title": "...", "author": { "id": "0x123" } - // like here ^^ - // or giving the data to create the object as part of a deep mutation - // { "title": "...", "author": { "username": "new user", "dob": "...", ... } - // like here ^^ - if fieldDef.Type().IsPoint() { + if fieldDef.Type().IsUnion() { + frags = rewriteUnionField(ctx, typ, fieldDef, myUID, varGen, + withAdditionalDeletes, val, deepXID, xidMetadata, -1) + } else if fieldDef.Type().IsPoint() { // For Point type, the mutation json in Dgraph is as follows: // { "type": "Point", "coordinates": [11.11, 22.22]} lat := val["latitude"] @@ -1227,6 +1223,13 @@ func rewriteObject( }, } } else { + // This field is another GraphQL object, which could either be linking to an + // existing node by it's ID + // { "title": "...", "author": { "id": "0x123" } + // like here ^^ + // or giving the data to create the object as part of a deep mutation + // { "title": "...", "author": { "username": "new user", "dob": "...", ... } + // like here ^^ frags = rewriteObject(ctx, typ, fieldDef.Type(), fieldDef, myUID, varGen, withAdditionalDeletes, val, deepXID, xidMetadata) @@ -1304,6 +1307,48 @@ func rewriteObject( return results } +// if this is a union field, then obj should have only one key which will be a ref +// to one of the member types. Eg: +// { "dogRef" : { ... } } +// So, just rewrite it as an object with correct underlying type. +func rewriteUnionField(ctx context.Context, + parentTyp schema.Type, + srcField schema.FieldDefinition, + srcUID string, + varGen *VariableGenerator, + withAdditionalDeletes bool, + obj map[string]interface{}, + deepXID int, + xidMetadata *xidMetadata, + listIndex int) *mutationRes { + if len(obj) != 1 { + errFrag := newFragment(nil) + // if this was called from rewriteList, + // the listIndex will tell which particular item in the list has an error. + if listIndex >= 0 { + errFrag.err = fmt.Errorf( + "value for field `%s` in type `%s` index `%d` must have exactly one child, "+ + "found %d children", srcField.Name(), parentTyp.Name(), listIndex, len(obj)) + } else { + errFrag.err = fmt.Errorf( + "value for field `%s` in type `%s` must have exactly one child, found %d children", + srcField.Name(), parentTyp.Name(), len(obj)) + } + return &mutationRes{secondPass: []*mutationFragment{errFrag}} + } + + var typ schema.Type + for memberRef, memberRefVal := range obj { + memberTypeName := strings.ToUpper(memberRef[:1]) + memberRef[1:len( + memberRef)-3] + srcField = srcField.WithMemberType(memberTypeName) + typ = srcField.Type() + obj = memberRefVal.(map[string]interface{}) + } + return rewriteObject(ctx, parentTyp, typ, srcField, srcUID, varGen, + withAdditionalDeletes, obj, deepXID, xidMetadata) +} + func invalidObjectFragment( err error, xidFrag *mutationFragment, @@ -1750,11 +1795,17 @@ func rewriteList( result.secondPass = []*mutationFragment{newFragment(make([]interface{}, 0))} foundSecondPass := false - for _, obj := range objects { + for i, obj := range objects { switch obj := obj.(type) { case map[string]interface{}: - frag := rewriteObject(ctx, parentTyp, typ, srcField, srcUID, varGen, - withAdditionalDeletes, obj, deepXID, xidMetadata) + var frag *mutationRes + if typ.IsUnion() { + frag = rewriteUnionField(ctx, parentTyp, srcField, srcUID, varGen, + withAdditionalDeletes, obj, deepXID, xidMetadata, i) + } else { + frag = rewriteObject(ctx, parentTyp, typ, srcField, srcUID, varGen, + withAdditionalDeletes, obj, deepXID, xidMetadata) + } if len(frag.secondPass) != 0 { foundSecondPass = true } diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index eacbff4de76..a5192c05f83 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -276,17 +276,17 @@ func rewriteAsQueryByIds(field schema.Field, uids []uint64, authRw *authRewriter } // addArgumentsToField adds various different arguments to a field, such as -// filter, order, pagination and selection set. +// filter, order and pagination. func addArgumentsToField(dgQuery *gql.GraphQuery, field schema.Field) { filter, _ := field.ArgValue("filter").(map[string]interface{}) - addFilter(dgQuery, field.Type(), filter) + _ = addFilter(dgQuery, field.Type(), filter) addOrder(dgQuery, field) addPagination(dgQuery, field) } func addFilterToField(dgQuery *gql.GraphQuery, field schema.Field) { filter, _ := field.ArgValue("filter").(map[string]interface{}) - addFilter(dgQuery, field.Type(), filter) + _ = addFilter(dgQuery, field.Type(), filter) } func addTopLevelTypeFilter(query *gql.GraphQuery, field schema.Field) { @@ -627,10 +627,7 @@ func (authRw *authRewriter) rewriteRuleNode( func addTypeFilter(q *gql.GraphQuery, typ schema.Type) { thisFilter := &gql.FilterTree{ - Func: &gql.Function{ - Name: "type", - Args: []gql.Arg{{Value: typ.DgraphName()}}, - }, + Func: buildTypeFunc(typ.DgraphName()), } addToFilterTree(q, thisFilter) } @@ -654,7 +651,11 @@ func addUIDFunc(q *gql.GraphQuery, uids []uint64) { } func addTypeFunc(q *gql.GraphQuery, typ string) { - q.Func = &gql.Function{ + q.Func = buildTypeFunc(typ) +} + +func buildTypeFunc(typ string) *gql.Function { + return &gql.Function{ Name: "type", Args: []gql.Arg{{Value: typ}}, } @@ -669,12 +670,12 @@ func addSelectionSetFrom( var authQueries []*gql.GraphQuery - // Only add dgraph.type as a child if this field is an interface type and has some children. - // dgraph.type would later be used in completeObject as different objects in the resulting - // JSON would return different fields based on their concrete type. selSet := field.SelectionSet() if len(selSet) > 0 { - if field.InterfaceType() { + // Only add dgraph.type as a child if this field is an abstract type and has some children. + // dgraph.type would later be used in completeObject as different objects in the resulting + // JSON would return different fields based on their concrete type. + if field.AbstractType() { q.Children = append(q.Children, &gql.GraphQuery{ Attr: "dgraph.type", }) @@ -716,14 +717,6 @@ func addSelectionSetFrom( continue } - // skip if we have already added a query for this field in DQL. It helps make sure that if - // a field is being asked twice or more, each time with a new alias, then we only add it - // once in DQL query. - if _, ok := fieldAdded[f.DgraphAlias()]; ok { - continue - } - fieldAdded[f.DgraphAlias()] = true - child := &gql.GraphQuery{ Alias: f.DgraphAlias(), } @@ -735,7 +728,10 @@ func addSelectionSetFrom( } filter, _ := f.ArgValue("filter").(map[string]interface{}) - addFilter(child, f.Type(), filter) + // if this field has been filtered out by the filter, then don't add it in DQL query + if includeField := addFilter(child, f.Type(), filter); !includeField { + continue + } addOrder(child, f) addPagination(child, f) addCascadeDirective(child, f) @@ -755,7 +751,6 @@ func addSelectionSetFrom( if !f.Type().IsPoint() { selectionAuth = addSelectionSetFrom(child, f, auth) } - addedFields[f.Name()] = true if len(f.SelectionSet()) > 0 && !auth.isWritingAuth && auth.hasAuthRules { // Restore the state after processing is done. @@ -763,6 +758,15 @@ func addSelectionSetFrom( auth.varName = parentQryName } + // skip if we have already added a query for this field in DQL. It helps make sure that if + // a field is being asked twice or more, each time with a new alias, then we only add it + // once in DQL query. + if _, ok := fieldAdded[f.DgraphAlias()]; ok { + continue + } + fieldAdded[f.DgraphAlias()] = true + addedFields[f.Name()] = true + if rbac == schema.Positive || rbac == schema.Uncertain { q.Children = append(q.Children, child) } @@ -952,9 +956,12 @@ func idFilter(filter map[string]interface{}, idField schema.FieldDefinition) []u return convertIDs(idsSlice) } -func addFilter(q *gql.GraphQuery, typ schema.Type, filter map[string]interface{}) { +// addFilter adds a filter to the input DQL query. It returns false if the field for which the +// filter was specified should not be included in the DQL query. +// Currently, it would only be false for a union field when no memberTypes are queried. +func addFilter(q *gql.GraphQuery, typ schema.Type, filter map[string]interface{}) bool { if len(filter) == 0 { - return + return true } // There are two cases here. @@ -974,10 +981,20 @@ func addFilter(q *gql.GraphQuery, typ schema.Type, filter map[string]interface{} // If id was present as a filter, delete(filter, idName) } - q.Filter = buildFilter(typ, filter) + + if typ.IsUnion() { + if filter, includeField := buildUnionFilter(typ, filter); includeField { + q.Filter = filter + } else { + return false + } + } else { + q.Filter = buildFilter(typ, filter) + } if filterAtRoot { addTypeFilter(q, typ) } + return true } // buildFilter builds a Dgraph gql.FilterTree from a GraphQL 'filter' arg. @@ -1163,6 +1180,44 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *gql.FilterTree } } +func buildUnionFilter(typ schema.Type, filter map[string]interface{}) (*gql.FilterTree, bool) { + memberTypesList, ok := filter["memberTypes"].([]interface{}) + // if memberTypes was specified to be an empty list like: { memberTypes: [], ...}, + // then we don't need to include the field, on which the filter was specified, in the query. + if ok && len(memberTypesList) == 0 { + return nil, false + } + + ft := &gql.FilterTree{ + Op: "or", + } + + // now iterate over the filtered member types for this union and build FilterTree for them + for _, memberType := range typ.UnionMembers(memberTypesList) { + memberTypeFilter, _ := filter[schema.CamelCase(memberType.Name())+"Filter"].(map[string]interface{}) + var memberTypeFt *gql.FilterTree + if len(memberTypeFilter) == 0 { + // if the filter for a member type wasn't specified, was null, or was specified as {}; + // then we need to query all nodes of that member type for the field on which the filter + // was specified. + memberTypeFt = &gql.FilterTree{Func: buildTypeFunc(memberType.DgraphName())} + } else { + // else we need to query only the nodes which match the filter for that member type + memberTypeFt = &gql.FilterTree{ + Op: "and", + Child: []*gql.FilterTree{ + {Func: buildTypeFunc(memberType.DgraphName())}, + buildFilter(memberType, memberTypeFilter), + }, + } + } + ft.Child = append(ft.Child, memberTypeFt) + } + + // return true because we want to include the field with filter in query + return ft, true +} + func maybeQuoteArg(fn string, arg interface{}) string { switch arg := arg.(type) { case string: // dateTime also parsed as string diff --git a/graphql/resolve/query_test.yaml b/graphql/resolve/query_test.yaml index a3efd655ba7..f3ffbf8db79 100644 --- a/graphql/resolve/query_test.yaml +++ b/graphql/resolve/query_test.yaml @@ -1806,4 +1806,183 @@ queryThingOne(func: type(ThingOne)) { dgraph.uid : uid } + } + +- name: "query union field - with fragment on interface implemented by member-types" + gqlquery: |- + query { + queryHome { + address + members { + ... on Animal { + category + } + ... on Dog { + breed + } + ... on Parrot { + repeatsWords + } + ... on Human { + name + dob + } + } + } + } + dgquery: |- + query { + queryHome(func: type(Home)) { + address : Home.address + members : Home.members { + dgraph.type + category : Animal.category + Dog.breed : Dog.breed + repeatsWords : Parrot.repeatsWords + name : Character.name + dob : Human.dob + dgraph.uid : uid + } + dgraph.uid : uid + } + } + +- name: "query union field - with repeated field in member-types" + gqlquery: |- + query { + queryHome { + members { + ... on Dog { + category + breed + } + ... on Plant { + breed + } + } + } + } + dgquery: |- + query { + queryHome(func: type(Home)) { + members : Home.members { + dgraph.type + category : Animal.category + Dog.breed : Dog.breed + Plant.breed : Plant.breed + dgraph.uid : uid + } + dgraph.uid : uid + } + } + +- name: "query union field - with arguments on union field" + gqlquery: |- + query { + queryHome { + members(filter: { + memberTypes: [Dog, Parrot] + dogFilter: { + breed: { allofterms: "German Shepherd"} + } + } + first: 5 + offset: 10 + ) { + ... on Dog { + id + } + ... on Parrot { + repeatsWords + } + } + } + } + dgquery: |- + query { + queryHome(func: type(Home)) { + members : Home.members @filter(((type(Dog) AND allofterms(Dog.breed, "German Shepherd")) OR type(Parrot))) (first: 5, offset: 10) { + dgraph.type + id : uid + repeatsWords : Parrot.repeatsWords + } + dgraph.uid : uid + } + } + +- name: "query union field - memberTypes is empty list" + gqlquery: |- + query { + queryHome { + members(filter: { + memberTypes: [] + dogFilter: { + breed: { allofterms: "German Shepherd"} + } + }) { + ... on Dog { + id + } + ... on Parrot { + repeatsWords + } + } + } + } + dgquery: |- + query { + queryHome(func: type(Home)) + } + +- name: "query union field - memberTypes isn't specified" + gqlquery: |- + query { + queryHome { + members(filter: { + dogFilter: { + breed: { allofterms: "German Shepherd"} + } + }) { + ... on Dog { + id + } + } + } + } + dgquery: |- + query { + queryHome(func: type(Home)) { + members : Home.members @filter(((type(Dog) AND allofterms(Dog.breed, "German Shepherd")) OR type(Parrot) OR type(Human) OR type(Plant))) { + dgraph.type + id : uid + } + dgraph.uid : uid + } + } + +- name: "query union field - memberTypes contains all the types" + gqlquery: |- + query { + queryHome { + members(filter: { + memberTypes: [Dog, Human, Parrot, Plant] + dogFilter: { + breed: { allofterms: "German Shepherd"} + } + }) { + ... on Dog { + id + } + } + } + } + dgquery: |- + query { + queryHome(func: type(Home)) { + members : Home.members @filter(((type(Dog) AND allofterms(Dog.breed, "German Shepherd")) OR type(Human) OR type(Parrot) OR type(Plant))) { + dgraph.type + id : uid + } + dgraph.uid : uid + } } \ No newline at end of file diff --git a/graphql/resolve/schema.graphql b/graphql/resolve/schema.graphql index 2d467a04457..eb7743a675c 100644 --- a/graphql/resolve/schema.graphql +++ b/graphql/resolve/schema.graphql @@ -297,4 +297,56 @@ type Person { interface A { name: String! @id -} \ No newline at end of file +} + +# union testing - start +enum AnimalCategory { + Fish + Amphibian + Reptile + Bird + Mammal + InVertebrate +} + +interface Animal { + id: ID! + category: AnimalCategory @search +} + +type Dog implements Animal { + breed: String @search +} + +type Parrot implements Animal { + repeatsWords: [String] +} + +type Cheetah implements Animal { + speed: Float +} + +""" +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 +} + +union HomeMember = Dog | Parrot | Human | Plant + +type Zoo { + id: ID! + animals: [Animal] + city: String +} + +type Home { + id: ID! + address: String + members: [HomeMember] + favouriteMember: HomeMember +} +# union testing - end \ No newline at end of file diff --git a/graphql/resolve/update_mutation_test.yaml b/graphql/resolve/update_mutation_test.yaml index 392247d79ae..4ed67a4fb8c 100644 --- a/graphql/resolve/update_mutation_test.yaml +++ b/graphql/resolve/update_mutation_test.yaml @@ -1516,3 +1516,87 @@ Computer5 as ComputerOwner.computers @filter(NOT (uid(Computer4))) } } + +- + name: "Add mutation with union" + gqlmutation: | + mutation($patch: UpdateHomeInput!) { + updateHome(input: $patch) { + home { + address + members { + ... on Dog { + breed + } + } + } + } + } + gqlvariables: | + { + "patch": { + "filter": { + "id": ["0x123"] + }, + "set": { + "address": "United Street", + "members": [ + { "dogRef": { "category": "Mammal", "breed": "German Shephard"} }, + { "parrotRef": { "category": "Bird", "repeatsWords": ["squawk"]} }, + { "humanRef": { "name": "Han Solo", "ename": "Han_emp"} } + ], + "favouriteMember": { "parrotRef": { "id": "0x124"} } + }, + "remove": { + "members": [ { "parrotRef": { "id": "0x125"} } ] + } + } + } + dgquery: |- + query { + x as updateHome(func: uid(0x123)) @filter(type(Home)) { + uid + } + Parrot3 as Parrot3(func: uid(0x124)) @filter(type(Parrot)) { + uid + } + Parrot8 as Parrot8(func: uid(0x125)) @filter(type(Parrot)) { + uid + } + } + dgmutations: + - setjson: | + { + "Home.address": "United Street", + "Home.favouriteMember": { + "uid": "0x124" + }, + "Home.members": [{ + "Animal.category": "Mammal", + "Dog.breed": "German Shephard", + "dgraph.type": ["Dog", "Animal"], + "uid": "_:Dog4" + }, { + "Animal.category": "Bird", + "Parrot.repeatsWords": ["squawk"], + "dgraph.type": ["Parrot", "Animal"], + "uid": "_:Parrot5" + }, { + "Character.name": "Han Solo", + "Employee.ename": "Han_emp", + "dgraph.type": ["Human", "Character", "Employee"], + "uid": "_:Human6" + }], + "uid": "uid(x)" + } + cond: "@if(eq(len(Parrot3), 1) AND gt(len(x), 0))" + - deletejson: | + { + "Home.members": [ + { + "uid": "0x125" + } + ], + "uid": "uid(x)" + } + cond: "@if(eq(len(Parrot8), 1) AND gt(len(x), 0))" diff --git a/graphql/schema/dgraph_schemagen_test.yml b/graphql/schema/dgraph_schemagen_test.yml index 0eb055c3375..81e0f85a272 100644 --- a/graphql/schema/dgraph_schemagen_test.yml +++ b/graphql/schema/dgraph_schemagen_test.yml @@ -298,6 +298,54 @@ schemas: D.link: uid . D.correct: bool . + - name: "Schema with union" + input: | + interface W { + f1: ID! + f7: U + } + type X implements W { + f2: String + } + type Y implements W { + f3: Int + } + type Z { + f4: Float + } + union P @remote = X | Y + union U = X | Y | Z + type V { + id: ID! + f5: [U!]! @dgraph(pred: "data") + f6: U + } + output: | + type W { + W.f7 + } + W.f7: uid . + type X { + W.f7 + X.f2 + } + X.f2: string . + type Y { + W.f7 + Y.f3 + } + Y.f3: int . + type Z { + Z.f4 + } + Z.f4: float . + type V { + data + V.f6 + } + data: [uid] . + V.f6: uid . + - name: "Schema with @dgraph directive." input: | diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index e78eb1de7cf..6bbc4dce664 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -34,19 +34,21 @@ const ( searchDirective = "search" searchArgs = "by" - dgraphDirective = "dgraph" - dgraphTypeArg = "type" - dgraphPredArg = "pred" + dgraphDirective = "dgraph" + dgraphTypeArg = "type" + dgraphPredArg = "pred" + idDirective = "id" + subscriptionDirective = "withSubscription" secretDirective = "secret" authDirective = "auth" customDirective = "custom" remoteDirective = "remote" // types with this directive are not stored in Dgraph. - cascadeDirective = "cascade" - cascadeArg = "fields" - SubscriptionDirective = "withSubscription" lambdaDirective = "lambda" + cascadeDirective = "cascade" + cascadeArg = "fields" + // custom directive args and fields dqlArg = "dql" httpArg = "http" @@ -161,7 +163,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION @@ -365,14 +367,29 @@ var directiveValidators = map[string]directiveValidator{ searchDirective: searchValidation, dgraphDirective: dgraphDirectiveValidation, idDirective: idValidation, + subscriptionDirective: ValidatorNoOp, secretDirective: passwordValidation, + authDirective: ValidatorNoOp, // Just to get it printed into generated schema customDirective: customDirectiveValidation, remoteDirective: ValidatorNoOp, deprecatedDirective: ValidatorNoOp, - SubscriptionDirective: ValidatorNoOp, - // Just go get it printed into generated schema - authDirective: ValidatorNoOp, - lambdaDirective: lambdaDirectiveValidation, + lambdaDirective: lambdaDirectiveValidation, +} + +// directiveLocationMap stores the directives and their locations for the ones which can be +// applied at type level in the user supplied schema. It is used during validation. +var directiveLocationMap = map[string]map[ast.DefinitionKind]bool{ + inverseDirective: nil, + searchDirective: nil, + dgraphDirective: {ast.Object: true, ast.Interface: true}, + idDirective: nil, + subscriptionDirective: {ast.Object: true, ast.Interface: true}, + secretDirective: {ast.Object: true, ast.Interface: true}, + authDirective: {ast.Object: true}, + customDirective: nil, + remoteDirective: {ast.Object: true, ast.Interface: true, ast.Union: true, + ast.InputObject: true, ast.Enum: true}, + cascadeDirective: nil, } var schemaDocValidations []func(schema *ast.SchemaDocument) gqlerror.List @@ -576,6 +593,15 @@ func completeSchema(sch *ast.Schema, definitions []string) { continue } defn := sch.Types[key] + if defn.Kind == ast.Union { + // TODO: properly check the case of reverse predicates (~) with union members and clean + // them from unionRef or unionFilter as required. + addUnionReferenceType(sch, defn) + addUnionFilterType(sch, defn) + addUnionMemberTypeEnum(sch, defn) + continue + } + if defn.Kind != ast.Interface && defn.Kind != ast.Object { continue } @@ -630,6 +656,7 @@ func cleanupInput(sch *ast.Schema, def *ast.Definition, seen map[string]bool) { seen[def.Name] = true cleanupInput(sch, sch.Types[nt], seen) + // If after calling cleanup on an input type, it got deleted then it doesn't need to be // in the fields for this type anymore. if sch.Types[nt] == nil { @@ -682,6 +709,60 @@ func cleanSchema(sch *ast.Schema) { sch.Mutation.Fields = sch.Mutation.Fields[:i] } +func addUnionReferenceType(schema *ast.Schema, defn *ast.Definition) { + refTypeName := defn.Name + "Ref" + refType := &ast.Definition{ + Kind: ast.InputObject, + Name: refTypeName, + } + for _, typName := range defn.Types { + refType.Fields = append(refType.Fields, &ast.FieldDefinition{ + Name: CamelCase(typName) + "Ref", + // the TRef for every member type is guaranteed to exist because member types can + // only be objects types, and a TRef is always generated for an object type + Type: &ast.Type{NamedType: typName + "Ref"}, + }) + } + schema.Types[refTypeName] = refType +} + +func addUnionFilterType(schema *ast.Schema, defn *ast.Definition) { + filterName := defn.Name + "Filter" + filter := &ast.Definition{ + Kind: ast.InputObject, + Name: defn.Name + "Filter", + Fields: []*ast.FieldDefinition{ + // field for selecting the union member type to report back + { + Name: "memberTypes", + Type: &ast.Type{Elem: &ast.Type{NamedType: defn.Name + "Type", NonNull: true}}, + }, + }, + } + // adding fields for specifying type filter for each union member type + for _, typName := range defn.Types { + filter.Fields = append(filter.Fields, &ast.FieldDefinition{ + Name: CamelCase(typName) + "Filter", + // the TFilter for every member type is guaranteed to exist because each member type + // will either have an ID field or some other kind of field causing it to have hasFilter + Type: &ast.Type{NamedType: typName + "Filter"}, + }) + } + schema.Types[filterName] = filter +} + +func addUnionMemberTypeEnum(schema *ast.Schema, defn *ast.Definition) { + enumName := defn.Name + "Type" + enum := &ast.Definition{ + Kind: ast.Enum, + Name: enumName, + } + for _, typName := range defn.Types { + enum.EnumValues = append(enum.EnumValues, &ast.EnumValueDefinition{Name: typName}) + } + schema.Types[enumName] = enum +} + func addInputType(schema *ast.Schema, defn *ast.Definition) { field := getFieldsWithoutIDType(schema, defn) if len(field) != 0 { @@ -819,12 +900,13 @@ func addFieldFilters(schema *ast.Schema, defn *ast.Definition) { } func addFilterArgument(schema *ast.Schema, fld *ast.FieldDefinition) { - fldType := fld.Type.Name() - if hasFilterable(schema.Types[fldType]) { + fldTypeName := fld.Type.Name() + fldType := schema.Types[fldTypeName] + if fldType.Kind == ast.Union || hasFilterable(fldType) { fld.Arguments = append(fld.Arguments, &ast.ArgumentDefinition{ Name: "filter", - Type: &ast.Type{NamedType: fldType + "Filter"}, + Type: &ast.Type{NamedType: fldTypeName + "Filter"}, }) } } @@ -1145,7 +1227,7 @@ func addTypeOrderable(schema *ast.Schema, defn *ast.Definition) { func addAddPayloadType(schema *ast.Schema, defn *ast.Definition) { qry := &ast.FieldDefinition{ - Name: camelCase(defn.Name), + Name: CamelCase(defn.Name), Type: ast.ListType(&ast.Type{ NamedType: defn.Name, }, nil), @@ -1176,7 +1258,7 @@ func addUpdatePayloadType(schema *ast.Schema, defn *ast.Definition) { } qry := &ast.FieldDefinition{ - Name: camelCase(defn.Name), + Name: CamelCase(defn.Name), Type: &ast.Type{ Elem: &ast.Type{ NamedType: defn.Name, @@ -1203,7 +1285,7 @@ func addDeletePayloadType(schema *ast.Schema, defn *ast.Definition) { } qry := &ast.FieldDefinition{ - Name: camelCase(defn.Name), + Name: CamelCase(defn.Name), Type: ast.ListType(&ast.Type{ NamedType: defn.Name, }, nil), @@ -1262,7 +1344,7 @@ func addGetQuery(schema *ast.Schema, defn *ast.Definition) { }) } schema.Query.Fields = append(schema.Query.Fields, qry) - subs := defn.Directives.ForName(SubscriptionDirective) + subs := defn.Directives.ForName(subscriptionDirective) if subs != nil { schema.Subscription.Fields = append(schema.Subscription.Fields, qry) } @@ -1282,7 +1364,7 @@ func addFilterQuery(schema *ast.Schema, defn *ast.Definition) { addPaginationArguments(qry) schema.Query.Fields = append(schema.Query.Fields, qry) - subs := defn.Directives.ForName(SubscriptionDirective) + subs := defn.Directives.ForName(subscriptionDirective) if subs != nil { schema.Subscription.Fields = append(schema.Subscription.Fields, qry) } @@ -1410,8 +1492,8 @@ func addMutations(schema *ast.Schema, defn *ast.Definition) { } func createField(schema *ast.Schema, fld *ast.FieldDefinition) *ast.FieldDefinition { - if schema.Types[fld.Type.Name()].Kind == ast.Object || - schema.Types[fld.Type.Name()].Kind == ast.Interface { + fieldTypeKind := schema.Types[fld.Type.Name()].Kind + if fieldTypeKind == ast.Object || fieldTypeKind == ast.Interface || fieldTypeKind == ast.Union { newDefn := &ast.FieldDefinition{ Name: fld.Name, } @@ -1638,7 +1720,9 @@ func genArgumentString(arg *ast.Argument) string { } func generateInputString(typ *ast.Definition) string { - return fmt.Sprintf("input %s {\n%s}\n", typ.Name, genFieldsString(typ.Fields)) + return fmt.Sprintf("%sinput %s%s {\n%s}\n", + generateDescription(typ.Description), typ.Name, genDirectivesString(typ.Directives), + genFieldsString(typ.Fields)) } func generateEnumString(typ *ast.Definition) string { @@ -1685,6 +1769,12 @@ func generateObjectString(typ *ast.Definition) string { genFieldsString(typ.Fields)) } +func generateUnionString(typ *ast.Definition) string { + return fmt.Sprintf("%sunion %s%s = %s\n", + generateDescription(typ.Description), typ.Name, genDirectivesString(typ.Directives), + strings.Join(typ.Types, " | ")) +} + // Stringify the schema as a GraphQL SDL string. It's assumed that the schema was // built by completeSchema, and so contains an original set of definitions, the // definitions from schemaExtras and generated types, queries and mutations. @@ -1701,8 +1791,8 @@ func Stringify(schema *ast.Schema, originalTypes []string) string { printed := make(map[string]bool) - // original defs can only be types and enums, print those in the same order - // as the original schema. + // original defs can only be interface, type, union, enum or input. + // print those in the same order as the original schema. for _, typName := range originalTypes { if isQueryOrMutation(typName) { // These would be printed later in schema.Query and schema.Mutation @@ -1714,6 +1804,8 @@ func Stringify(schema *ast.Schema, originalTypes []string) string { x.Check2(original.WriteString(generateInterfaceString(typ) + "\n")) case ast.Object: x.Check2(original.WriteString(generateObjectString(typ) + "\n")) + case ast.Union: + x.Check2(original.WriteString(generateUnionString(typ) + "\n")) case ast.Enum: x.Check2(original.WriteString(generateEnumString(typ) + "\n")) case ast.InputObject: @@ -1841,7 +1933,7 @@ func isGraphqlSpecScalar(typ string) bool { return ok } -func camelCase(x string) string { +func CamelCase(x string) string { if x == "" { return "" } diff --git a/graphql/schema/gqlschema_test.yml b/graphql/schema/gqlschema_test.yml index f0365588c12..01aa86e37bd 100644 --- a/graphql/schema/gqlschema_test.yml +++ b/graphql/schema/gqlschema_test.yml @@ -122,9 +122,124 @@ invalid_schemas: x: X! } errlist: [ - {"message":"You can't add scalar definitions. Only type, interface, input and enums are allowed in initial schema.", "locations":[{"line":1, "column":8}]}, - {"message":"You can't add union definitions. Only type, interface, input and enums are allowed in initial schema.", "locations":[{"line":5, "column":7}]}, - # {"message":"You can't add input_object definitions. Only type, interface, input and enums are allowed in initial schema.", "locations":[{"line":6, "column":7}]}, + {"message":"You can't add scalar definitions. Only type, interface, union, input and enums are allowed in initial schema.", "locations":[{"line":1, "column":8}]} + ] + + - + name: "union members can't be non-object types - Interface" + input: | + interface I { + f: String + } + union U = I + errlist: [ + {"message":"UNION type \"I\" must be OBJECT.", "locations":[{"line":4, "column":7}]} + ] + + - + name: "union members can't be non-object types - Scalar" + input: | + union U = String + errlist: [ + {"message":"UNION type \"String\" must be OBJECT.", "locations":[{"line":1, "column":7}]} + ] + + - + name: "union members can't be non-object types - Enum" + input: | + enum E { + E1 + E2 + } + union U = E + errlist: [ + {"message":"UNION type \"E\" must be OBJECT.", "locations":[{"line":5, "column":7}]} + ] + + - + name: "union members can't be non-object types - Input Object" + input: | + input I { + f: String + } + union U = I + errlist: [ + {"message":"UNION type \"I\" must be OBJECT.", "locations":[{"line":4, "column":7}]} + ] + + - + name: "union can't be used with @dgraph(type: ...)" + input: | + type X { + f1: String + } + type Y { + f2: Int + } + union U @dgraph(type: "U") = X | Y + errlist: [ + {"message":"Type U; has the @dgraph directive, but it is not applicable on types of UNION kind.", "locations":[{"line":7, "column":10}]} + ] + + - + name: "union can't be used with @withSubscription" + input: | + type X { + f1: String + } + type Y { + f2: Int + } + union U @withSubscription = X | Y + errlist: [ + { "message": "Type U; has the @withSubscription directive, but it is not applicable on types of UNION kind.", "locations":[{"line":7, "column":10}]} + ] + + - + name: "union can't be used with @secret" + input: | + type X { + f1: String + } + type Y { + f2: Int + } + union U @secret(field: "f2") = X | Y + errlist: [ + { "message": "Type U; has the @secret directive, but it is not applicable on types of UNION kind.", "locations":[{"line":7, "column":10}]} + ] + + - + name: "union can't be used with @auth" + input: | + type X { + f1: String + } + type Y { + f2: Int + } + union U @auth(query: {}) = X | Y + errlist: [ + { "message": "Type U; has the @auth directive, but it is not applicable on types of UNION kind.", "locations":[{"line":7, "column":10}]} + ] + + - + name: "union can't be used with @hasInverse, @search, @id" + input: | + type X { + f1: String + } + type Y { + f2: Int + } + union U = X | Y + type Z { + f: U @hasInverse(field: "f1") @search @id + } + 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!, not U", "locations": [{"line":9, "column":42}]} ] - @@ -133,8 +248,26 @@ invalid_schemas: type String { id: ID! } + type X { + f: Int + } + union Query = X + interface Mutation { + f: ID! + } + input Subscription { + name: String + } + enum uid { + E1 + E2 + } errlist: [ {"message":"String is a reserved word, so you can't declare a type with this name. Pick a different name for the type.", "locations":[{"line":1, "column":6}]}, + {"message":"Query is a reserved word, so you can't declare a type with this name. Pick a different name for the type.", "locations":[{"line":7, "column":7}]}, + {"message":"Mutation is a reserved word, so you can't declare a type with this name. Pick a different name for the type.", "locations":[{"line":8, "column":11}]}, + {"message":"Subscription is a reserved word, so you can't declare a type with this name. Pick a different name for the type.", "locations":[{"line":11, "column":7}]}, + {"message":"uid is a reserved word, so you can't declare a type with this name. Pick a different name for the type.", "locations":[{"line":14, "column":6}]}, ] - name: "Point is reserved word" @@ -161,14 +294,6 @@ invalid_schemas: {"message": "Type X; Field l2: ID lists are invalid.", "locations": [{"line": 6, "column": 3}]} ] - - - name: "Union type in schema" - input: | - union U = R | S | T - errlist: [ - {"message":"You can't add union definitions. Only type, interface, input and enums are allowed in initial schema.", "locations":[{"line":1, "column":7}]} - ] - - name: "Non linking inverse directive with correct field type" input: | @@ -571,8 +696,7 @@ invalid_schemas: f1: String! @dgraph(pred: 2) } errlist: [ - {"message": "Type X; Field f1: pred argument for @dgraph directive should - of type String.", + {"message": "Type X; Field f1: pred argument for @dgraph directive should be of type String.", "locations":[{"line":2, "column":16}]} ] @@ -831,7 +955,7 @@ invalid_schemas: {"message": "Type Y; Field f10: has the @dgraph predicate, but that conflicts with type W @secret directive on the same predicate. @secret predicates are stored encrypted and so the same predicate can't be used as a String.", "locations":[{"line":22, "column":3}]}] - - name: "input type can't have same name as the type generated for other types" + - name: "user-defined types can't have same name as the types generated for other user-defined types or any inbuilt types" input: | type Author { id: ID! @@ -841,9 +965,17 @@ invalid_schemas: id: ID! name: String } + union U = Author + input URef { + id: ID! + } + type IntFilter { + name: String + } errlist: [ - {"message": "UpdateAuthorInput is a reserved word, so you can't declare an input type with this name. Pick a different name for the input type.", - "locations":[{"line":5, "column":7}]}, + {"message": "UpdateAuthorInput is a reserved word, so you can't declare a INPUT_OBJECT with this name. Pick a different name for the INPUT_OBJECT.", "locations":[{"line":5, "column":7}]}, + {"message": "URef is a reserved word, so you can't declare a INPUT_OBJECT with this name. Pick a different name for the INPUT_OBJECT.", "locations":[{"line":10, "column":7}]}, + {"message": "IntFilter is a reserved word, so you can't declare a OBJECT with this name. Pick a different name for the OBJECT.", "locations":[{"line":13, "column":6}]}, ] - name: "@custom query can't have same name as the query generated for other types" @@ -2369,8 +2501,8 @@ invalid_schemas: userRole: String @search(by: [hash]) } errlist: [ - {"message": "Interface X; @auth directive is not allowed on interfaces.", - "locations":[{"line":1, "column":11}]}, + {"message": "Type X; has the @auth directive, but it is not applicable on types of INTERFACE kind.", + "locations":[{"line":1, "column":14}]}, ] @@ -2512,17 +2644,6 @@ invalid_schemas: "locations":[{"line":6, "column":5}]} ] - - - name: "Subscription typename should return error" - input: | - type Subscription { - name: String - } - - errlist: [ - {"message": "Subscription is a reserved word, so you can't declare a type with this name. Pick a different name for the type.", "locations": [{"line":1, "column":6}]}, - ] - - name: "as is reserved keyword - type Name" input: | @@ -2566,6 +2687,27 @@ invalid_schemas: valid_schemas: + - name: "schema with union" + input: | + interface W { + f1: ID! + } + type X implements W { + f2: String + } + type Y implements W { + f3: Int + } + type Z { + f4: Float + } + union P @remote = X | Y + union U = X | Y | Z + type V { + id: ID! + data: [U!]! @dgraph(pred: "data") + } + - name: "@auth on interface implementation" input: | interface X { diff --git a/graphql/schema/request.go b/graphql/schema/request.go index 12e302b3815..deafc84f95a 100644 --- a/graphql/schema/request.go +++ b/graphql/schema/request.go @@ -162,8 +162,26 @@ func recursivelyExpandFragmentSelections(field *ast.Field, op *operation) { addSelectionToInterfaceImplFragFields(typeName, f, additionalTypes, op) } case ast.Union: - // expand fragments on types of which it is a union + // expand fragments on member types of this union additionalTypes = getTypeNamesAsMap(op.inSchema.schema.PossibleTypes[typeName]) + // also, expand fragments on interfaces which are implemented by the member types of this + // union + var interfaceFragsToExpand []*ast.Definition + for memberType := range additionalTypes { + interfaceFragsToExpand = append(interfaceFragsToExpand, + op.inSchema.schema.Implements[memberType]...) + } + additionalInterfaces := getTypeNamesAsMap(interfaceFragsToExpand) + // for fragments in the selection set of this union field, need to store a mapping from + // fields in that fragment to the fragment's type condition, for each of the additional + // interfaces, to be used later in completion. + for interfaceName := range additionalInterfaces { + additionalTypes[interfaceName] = true + for _, f := range field.SelectionSet { + addSelectionToInterfaceImplFragFields(interfaceName, f, + getTypeNamesAsMap(op.inSchema.schema.PossibleTypes[interfaceName]), op) + } + } case ast.Object: // expand fragments on interfaces which are implemented by this object additionalTypes = getTypeNamesAsMap(op.inSchema.schema.Implements[typeName]) @@ -266,11 +284,16 @@ func addFragFieldsToInterfaceImplFields(interfaceTypeName, typeCond string, selS // otherwise, if the type condition is same as the type of the interface, // then we still need to look if there are any more fragments inside this fragment for _, fragField := range selSet { - if _, ok := fragField.(*ast.Field); !ok { + if f, ok := fragField.(*ast.Field); !ok { // we got a fragment inside fragment // the type condition for this fragment may be different that its parent fragment addSelectionToInterfaceImplFragFields(interfaceTypeName, fragField, interfaceImplMap, op) + } else { + // we got a field on an interface, so save the mapping of field + // to the interface type name. This will later be used during completion to find + // out if the field should be reported back in the response or not. + op.interfaceImplFragFields[f] = interfaceTypeName } } } diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index b494fc9bc16..cc3adf704fc 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -32,9 +32,9 @@ import ( ) func init() { - schemaDocValidations = append(schemaDocValidations, inputTypeNameValidation, + schemaDocValidations = append(schemaDocValidations, typeNameValidation, customQueryNameValidation, customMutationNameValidation) - defnValidations = append(defnValidations, dataTypeCheck, nameCheck) + defnValidations = append(defnValidations, dataTypeCheck, nameCheck, directiveLocationCheck) schemaValidations = append(schemaValidations, dgraphDirectivePredicateValidation) typeValidations = append(typeValidations, idCountCheck, dgraphDirectiveTypeValidation, @@ -114,15 +114,6 @@ func dgraphDirectivePredicateValidation(gqlSch *ast.Schema, definitions []string } } - checkConflictingDirectivesOnInterface := func(def *ast.Definition) { - for _, directive := range def.Directives { - if directive.Name == authDirective { - errs = append(errs, gqlerror.ErrorPosf(def.Position, - "Interface %s; @auth directive is not allowed on interfaces.", def.Name)) - } - } - } - checkConflictingFieldsInImplementedInterfacesError := func(typ *ast.Definition) { fieldsToReport := make(map[string][]string) interfaces := typ.Interfaces @@ -169,7 +160,6 @@ func dgraphDirectivePredicateValidation(gqlSch *ast.Schema, definitions []string typName := typeName(def) if def.Kind == ast.Interface { interfacePreds[def.Name] = make(map[string]bool) - checkConflictingDirectivesOnInterface(def) } else { checkConflictingFieldsInImplementedInterfacesError(def) } @@ -257,17 +247,21 @@ func dgraphDirectivePredicateValidation(gqlSch *ast.Schema, definitions []string return errs } -func inputTypeNameValidation(schema *ast.SchemaDocument) gqlerror.List { +// typeNameValidation checks that no user-defined type can have a name that may be +// statically/dynamically generated by us +func typeNameValidation(schema *ast.SchemaDocument) gqlerror.List { var errs []*gqlerror.Error - forbiddenInputTypeNames := map[string]bool{ - // The types that we define in schemaExtras + forbiddenTypeNames := map[string]bool{ + // The static types that we define in schemaExtras "Int64": true, "DateTime": true, "DgraphIndex": true, + "AuthRule": true, "HTTPMethod": true, + "Mode": true, "CustomHTTP": true, - "CustomGraphQL": true, "IntFilter": true, + "Int64Filter": true, "FloatFilter": true, "DateTimeFilter": true, "StringTermFilter": true, @@ -279,18 +273,15 @@ func inputTypeNameValidation(schema *ast.SchemaDocument) gqlerror.List { "PointRef": true, "NearFilter": true, } - definedInputTypes := make([]*ast.Definition, 0) for _, defn := range schema.Definitions { - defName := defn.Name - if isQueryOrMutation(defName) { - continue - } - if defn.Kind == ast.InputObject { - definedInputTypes = append(definedInputTypes, defn) + // prelude definitions are built in and we don't want to validate them. + if defn.BuiltIn { continue } - if defn.Kind != ast.Object && defn.Kind != ast.Interface { + + defName := defn.Name + if isQueryOrMutation(defName) { continue } @@ -301,28 +292,37 @@ func inputTypeNameValidation(schema *ast.SchemaDocument) gqlerror.List { continue } - // types that are generated by us - forbiddenInputTypeNames[defName+"Ref"] = true - forbiddenInputTypeNames[defName+"Patch"] = true - forbiddenInputTypeNames["Update"+defName+"Input"] = true - forbiddenInputTypeNames["Update"+defName+"Payload"] = true - forbiddenInputTypeNames["Delete"+defName+"Input"] = true + // adding the types that we dynamically generate to forbidden names + switch { + case defn.Kind == ast.Union: + // for unions we generate only `Ref` and `Filter` inputs and a `Type` enum + forbiddenTypeNames[defName+"Ref"] = true + forbiddenTypeNames[defName+"Filter"] = true + forbiddenTypeNames[defName+"Type"] = true + case defn.Kind == ast.Object || defn.Kind == ast.Interface: + // types that are generated by us for objects and interfaces + forbiddenTypeNames[defName+"Ref"] = true + forbiddenTypeNames[defName+"Patch"] = true + forbiddenTypeNames["Update"+defName+"Input"] = true + forbiddenTypeNames["Update"+defName+"Payload"] = true + forbiddenTypeNames["Delete"+defName+"Input"] = true + + if defn.Kind == ast.Object { + forbiddenTypeNames["Add"+defName+"Input"] = true + forbiddenTypeNames["Add"+defName+"Payload"] = true + } - if defn.Kind == ast.Object { - forbiddenInputTypeNames["Add"+defName+"Input"] = true - forbiddenInputTypeNames["Add"+defName+"Payload"] = true + forbiddenTypeNames[defName+"Filter"] = true + forbiddenTypeNames[defName+"Order"] = true + forbiddenTypeNames[defName+"Orderable"] = true } - - forbiddenInputTypeNames[defName+"Filter"] = true - forbiddenInputTypeNames[defName+"Order"] = true - forbiddenInputTypeNames[defName+"Orderable"] = true } - for _, inputType := range definedInputTypes { - if forbiddenInputTypeNames[inputType.Name] { - errs = append(errs, gqlerror.ErrorPosf(inputType.Position, - "%s is a reserved word, so you can't declare an input type with this name. "+ - "Pick a different name for the input type.", inputType.Name)) + for _, typ := range schema.Definitions { + if !typ.BuiltIn && forbiddenTypeNames[typ.Name] { + errs = append(errs, gqlerror.ErrorPosf(typ.Position, + "%s is a reserved word, so you can't declare a %s with this name. "+ + "Pick a different name for the %s.", typ.Name, typ.Kind, typ.Kind)) } } @@ -414,19 +414,16 @@ func customMutationNameValidation(schema *ast.SchemaDocument) gqlerror.List { } func dataTypeCheck(schema *ast.Schema, defn *ast.Definition) gqlerror.List { - if defn.Kind == ast.Object || defn.Kind == ast.Enum || defn.Kind == ast.Interface || defn. - Kind == ast.InputObject { - return nil + if defn.Kind == ast.Scalar { + return []*gqlerror.Error{gqlerror.ErrorPosf( + defn.Position, "You can't add scalar definitions. "+ + "Only type, interface, union, input and enums are allowed in initial schema.")} } - return []*gqlerror.Error{gqlerror.ErrorPosf( - defn.Position, - "You can't add %s definitions. "+ - "Only type, interface, input and enums are allowed in initial schema.", - strings.ToLower(string(defn.Kind)))} + return nil } func nameCheck(schema *ast.Schema, defn *ast.Definition) gqlerror.List { - if (defn.Kind == ast.Object || defn.Kind == ast.Enum) && isReservedKeyWord(defn.Name) { + if defn.Kind != ast.Scalar && isReservedKeyWord(defn.Name) { var errMesg string if isQueryOrMutationType(defn) { @@ -456,6 +453,30 @@ func nameCheck(schema *ast.Schema, defn *ast.Definition) gqlerror.List { return nil } +// This could be removed once the following gqlparser bug is fixed: +// https://github.com/vektah/gqlparser/issues/128 +func directiveLocationCheck(schema *ast.Schema, defn *ast.Definition) gqlerror.List { + var errs []*gqlerror.Error + for _, dir := range defn.Directives { + dirLocInfo, ok := directiveLocationMap[dir.Name] + if !ok { + continue + } + if dirLocInfo == nil { + errs = append(errs, gqlerror.ErrorPosf( + dir.Position, "Type %s; has the @%s directive, "+ + "but it is not applicable at type level.", defn.Name, dir.Name)) + continue + } + if !dirLocInfo[defn.Kind] { + errs = append(errs, gqlerror.ErrorPosf( + dir.Position, "Type %s; has the @%s directive, "+ + "but it is not applicable on types of %s kind.", defn.Name, dir.Name, defn.Kind)) + } + } + return errs +} + func collectFieldNames(idFields []*ast.FieldDefinition) (string, []gqlerror.Location) { var fieldNames []string var errLocations []gqlerror.Location @@ -484,7 +505,7 @@ func conflictingDirectiveValidation(schema *ast.Schema, typ *ast.Definition) gql if dir.Name == remoteDirective { hasRemote = true } - if dir.Name == SubscriptionDirective { + if dir.Name == subscriptionDirective { hasSubscription = true } } @@ -494,7 +515,7 @@ func conflictingDirectiveValidation(schema *ast.Schema, typ *ast.Definition) gql } if hasSubscription && hasRemote { return []*gqlerror.Error{gqlerror.ErrorPosf(typ.Position, `Type %s; cannot have both @%s and @%s directive`, - typ.Name, SubscriptionDirective, remoteDirective)} + typ.Name, subscriptionDirective, remoteDirective)} } return nil } @@ -573,8 +594,7 @@ func dgraphDirectiveTypeValidation(schema *ast.Schema, typ *ast.Definition) gqle // 2. Fields with @custom directive. // to be a valid type. Otherwise its not possible to add objects of that type. func nonIdFieldsCheck(schema *ast.Schema, typ *ast.Definition) gqlerror.List { - if isQueryOrMutation(typ.Name) || typ.Kind == ast.Enum || typ.Kind == ast.Interface || - typ.Kind == ast.InputObject { + if typ.Kind != ast.Object || isQueryOrMutationType(typ) { return nil } @@ -602,7 +622,7 @@ func nonIdFieldsCheck(schema *ast.Schema, typ *ast.Definition) gqlerror.List { } func remoteTypeValidation(schema *ast.Schema, typ *ast.Definition) gqlerror.List { - if isQueryOrMutation(typ.Name) { + if isQueryOrMutationType(typ) { return nil } remote := typ.Directives.ForName(remoteDirective) @@ -1067,7 +1087,7 @@ func dgraphDirectiveValidation(sch *ast.Schema, typ *ast.Definition, field *ast. if predArg.Value.Kind != ast.StringValue { errs = append(errs, gqlerror.ErrorPosf( dir.Position, - "Type %s; Field %s: pred argument for @dgraph directive should of type String.", + "Type %s; Field %s: pred argument for @dgraph directive should be of type String.", typ.Name, field.Name)) return errs } @@ -1159,7 +1179,7 @@ func dgraphDirectiveValidation(sch *ast.Schema, typ *ast.Definition, field *ast. if invDirective != nil { errs = append(errs, gqlerror.ErrorPosf( dir.Position, - "Type %s; Field %s: @hasInverse directive is not allowed is not allowed "+ + "Type %s; Field %s: @hasInverse directive is not allowed "+ "because field is forward edge of another field with reverse directive.", invType.Name, fld.Name)) return errs @@ -1922,7 +1942,7 @@ func isReservedKeyWord(name string) bool { } func isQueryOrMutationType(typ *ast.Definition) bool { - return isQueryOrMutation(typ.Name) + return typ.Kind == ast.Object && isQueryOrMutation(typ.Name) } func isQueryOrMutation(name string) bool { diff --git a/graphql/schema/schemagen.go b/graphql/schema/schemagen.go index 163d7d8f4e7..60afc0fd985 100644 --- a/graphql/schema/schemagen.go +++ b/graphql/schema/schemagen.go @@ -185,7 +185,7 @@ func NewHandler(input string, validateOnly bool) (Handler, error) { continue } defns = append(defns, defn.Name) - if defn.Kind == ast.Object || defn.Kind == ast.Interface { + if defn.Kind == ast.Object || defn.Kind == ast.Interface || defn.Kind == ast.Union { remoteDir := defn.Directives.ForName(remoteDirective) if remoteDir != nil { continue @@ -446,7 +446,7 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string) string { var typStr string switch gqlSch.Types[f.Type.Name()].Kind { - case ast.Object, ast.Interface: + case ast.Object, ast.Interface, ast.Union: if isPointType(f.Type) { typStr = "geo" var indexes []string diff --git a/graphql/schema/testdata/schemagen/input/comments-and-descriptions.graphql b/graphql/schema/testdata/schemagen/input/comments-and-descriptions.graphql index 48729dc4275..856d2a0111c 100644 --- a/graphql/schema/testdata/schemagen/input/comments-and-descriptions.graphql +++ b/graphql/schema/testdata/schemagen/input/comments-and-descriptions.graphql @@ -35,4 +35,22 @@ enum AnEnum { Desc """ AnotherVal +} + +""" +Desc +""" +union A_Union = T + +""" +Desc +""" +input AnInput { + # # comment + id: ID! # comment + + """ + Desc + """ + i: Int } \ No newline at end of file diff --git a/graphql/schema/testdata/schemagen/input/union.graphql b/graphql/schema/testdata/schemagen/input/union.graphql new file mode 100644 index 00000000000..ee36d9f3cc7 --- /dev/null +++ b/graphql/schema/testdata/schemagen/input/union.graphql @@ -0,0 +1,41 @@ +interface Character { + id: ID! + name: String! @search(by: [exact]) + friends: [Character] + enemyOf: Resident + appearsIn: [Episode!]! @search +} + +type Human implements Character { + starships: [Starship] + totalCredits: Int +} + +type Droid implements Character { + primaryFunction: String +} + +enum Episode { + NEWHOPE + EMPIRE + JEDI +} + +type Starship { + id: ID! + name: String! @search(by: [term]) + length: Float +} + +union Resident = Human | Droid | Starship +union Tool @remote = Droid | Starship + +type Planet { + id: ID! + name: String! + residents: [Resident!] @dgraph(pred: "residents") + bestTool: Tool @custom(http: { + url: "http://mock:8888/tool/$id" + method: "GET" + }) +} \ No newline at end of file diff --git a/graphql/schema/testdata/schemagen/output/authorization.graphql b/graphql/schema/testdata/schemagen/output/authorization.graphql index c2210d4cd10..d596ae7beb7 100644 --- a/graphql/schema/testdata/schemagen/output/authorization.graphql +++ b/graphql/schema/testdata/schemagen/output/authorization.graphql @@ -115,7 +115,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql b/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql index 76e29b7aa29..78b97f2eb40 100755 --- a/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql +++ b/graphql/schema/testdata/schemagen/output/comments-and-descriptions.graphql @@ -23,6 +23,16 @@ enum AnEnum { AnotherVal } +"""Desc""" +union A_Union = T + +"""Desc""" +input AnInput { + id: ID! + """Desc""" + i: Int +} + ####################### # Extended Definitions ####################### @@ -120,7 +130,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION @@ -208,6 +218,10 @@ type UpdateTPayload { # Generated Enums ####################### +enum A_UnionType { + T +} + enum IHasFilter { s } @@ -230,6 +244,15 @@ enum TOrderable { # Generated Inputs ####################### +input A_UnionFilter { + memberTypes: [A_UnionType!] + tFilter: TFilter +} + +input A_UnionRef { + tRef: TRef +} + input AddTInput { s: String! """Desc""" diff --git a/graphql/schema/testdata/schemagen/output/custom-mutation.graphql b/graphql/schema/testdata/schemagen/output/custom-mutation.graphql index fa5705a324a..ae925b1b090 100644 --- a/graphql/schema/testdata/schemagen/output/custom-mutation.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-mutation.graphql @@ -108,7 +108,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql b/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql index 5ca57afdbbf..374d9c4abfb 100755 --- a/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-nested-types.graphql @@ -125,7 +125,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql b/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql index cf5e577a52b..1b74b7f3fff 100644 --- a/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-query-mixed-types.graphql @@ -109,7 +109,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/custom-query-not-dgraph-type.graphql b/graphql/schema/testdata/schemagen/output/custom-query-not-dgraph-type.graphql index 1619f761b9a..c4dc94e8f00 100755 --- a/graphql/schema/testdata/schemagen/output/custom-query-not-dgraph-type.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-query-not-dgraph-type.graphql @@ -108,7 +108,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/custom-query-with-dgraph-type.graphql b/graphql/schema/testdata/schemagen/output/custom-query-with-dgraph-type.graphql index a0b28a34de7..e9481745063 100755 --- a/graphql/schema/testdata/schemagen/output/custom-query-with-dgraph-type.graphql +++ b/graphql/schema/testdata/schemagen/output/custom-query-with-dgraph-type.graphql @@ -104,7 +104,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/deprecated.graphql b/graphql/schema/testdata/schemagen/output/deprecated.graphql index c4fa86a1e15..25e3600da72 100755 --- a/graphql/schema/testdata/schemagen/output/deprecated.graphql +++ b/graphql/schema/testdata/schemagen/output/deprecated.graphql @@ -104,7 +104,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-on-concrete-type-with-interfaces.graphql b/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-on-concrete-type-with-interfaces.graphql index 04b8053f5bd..a32b3f5982a 100755 --- a/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-on-concrete-type-with-interfaces.graphql +++ b/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-on-concrete-type-with-interfaces.graphql @@ -118,7 +118,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-with-interfaces.graphql b/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-with-interfaces.graphql index 94ee543f3de..b71eff4ef9d 100755 --- a/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-with-interfaces.graphql +++ b/graphql/schema/testdata/schemagen/output/dgraph-reverse-directive-with-interfaces.graphql @@ -118,7 +118,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION 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 3dd43054e22..9f38d264d53 100755 --- a/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/field-with-id-directive.graphql @@ -117,7 +117,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/field-with-reverse-predicate-in-dgraph-directive.graphql b/graphql/schema/testdata/schemagen/output/field-with-reverse-predicate-in-dgraph-directive.graphql index 66bb07a992c..62106bce455 100755 --- a/graphql/schema/testdata/schemagen/output/field-with-reverse-predicate-in-dgraph-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/field-with-reverse-predicate-in-dgraph-directive.graphql @@ -111,7 +111,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql index d7c0fd7db0b..b7ab124a4e7 100644 --- a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-all-empty.graphql @@ -112,7 +112,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql index 319c3628758..5697ff2317f 100644 --- a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-circular.graphql @@ -114,7 +114,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql index 74908392160..68399368957 100644 --- a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-custom-mutation.graphql @@ -108,7 +108,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql index fba6442f9a1..50027d0fb48 100644 --- a/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql +++ b/graphql/schema/testdata/schemagen/output/filter-cleanSchema-directLink.graphql @@ -114,7 +114,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/geo-type.graphql b/graphql/schema/testdata/schemagen/output/geo-type.graphql index 3329015702e..6448b6bd8cd 100644 --- a/graphql/schema/testdata/schemagen/output/geo-type.graphql +++ b/graphql/schema/testdata/schemagen/output/geo-type.graphql @@ -105,7 +105,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface-having-directive.graphql b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface-having-directive.graphql index 3b4557e2d49..52467cabbd7 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface-having-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface-having-directive.graphql @@ -128,7 +128,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql index 641db742198..e013cdc7397 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse-with-interface.graphql @@ -129,7 +129,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/hasInverse-with-type-having-directive.graphql b/graphql/schema/testdata/schemagen/output/hasInverse-with-type-having-directive.graphql index 3b4557e2d49..52467cabbd7 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse-with-type-having-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse-with-type-having-directive.graphql @@ -128,7 +128,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/hasInverse.graphql b/graphql/schema/testdata/schemagen/output/hasInverse.graphql index 1a603c4d78a..72c52811752 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse.graphql @@ -109,7 +109,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql b/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql index 727ba3b8e23..f29ee1297c1 100755 --- a/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql +++ b/graphql/schema/testdata/schemagen/output/hasInverse_withSubscription.graphql @@ -109,7 +109,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/hasfilter.graphql b/graphql/schema/testdata/schemagen/output/hasfilter.graphql index 6cf229cf2e8..fd272fae526 100644 --- a/graphql/schema/testdata/schemagen/output/hasfilter.graphql +++ b/graphql/schema/testdata/schemagen/output/hasfilter.graphql @@ -112,7 +112,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql b/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql index a9c0e71e32d..aae542693dc 100755 --- a/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/ignore-unsupported-directive.graphql @@ -111,7 +111,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql b/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql index 3e88d7d1eeb..1b54a73bef8 100644 --- a/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-dgraph-pred.graphql @@ -118,7 +118,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION 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 2d25e680d45..987560bde73 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql @@ -113,7 +113,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql b/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql index 95444ce7cce..30a49a0b6d9 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-no-ids.graphql @@ -113,7 +113,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/interfaces-with-types-and-password.graphql b/graphql/schema/testdata/schemagen/output/interfaces-with-types-and-password.graphql index 65914f30a7c..ac4d49c5933 100755 --- a/graphql/schema/testdata/schemagen/output/interfaces-with-types-and-password.graphql +++ b/graphql/schema/testdata/schemagen/output/interfaces-with-types-and-password.graphql @@ -135,7 +135,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql b/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql index 20b52de0c57..a5242639794 100755 --- a/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql +++ b/graphql/schema/testdata/schemagen/output/interfaces-with-types.graphql @@ -135,7 +135,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/lambda-directive.graphql b/graphql/schema/testdata/schemagen/output/lambda-directive.graphql index a7439136fb3..46abcb38eab 100644 --- a/graphql/schema/testdata/schemagen/output/lambda-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/lambda-directive.graphql @@ -106,7 +106,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/no-id-field-with-searchables.graphql b/graphql/schema/testdata/schemagen/output/no-id-field-with-searchables.graphql index f25b36f1625..98ede0a7403 100755 --- a/graphql/schema/testdata/schemagen/output/no-id-field-with-searchables.graphql +++ b/graphql/schema/testdata/schemagen/output/no-id-field-with-searchables.graphql @@ -103,7 +103,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/no-id-field.graphql b/graphql/schema/testdata/schemagen/output/no-id-field.graphql index 7316f973d01..8d3783576f0 100755 --- a/graphql/schema/testdata/schemagen/output/no-id-field.graphql +++ b/graphql/schema/testdata/schemagen/output/no-id-field.graphql @@ -115,7 +115,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/password-type.graphql b/graphql/schema/testdata/schemagen/output/password-type.graphql index f22ee8000ff..3533da43641 100755 --- a/graphql/schema/testdata/schemagen/output/password-type.graphql +++ b/graphql/schema/testdata/schemagen/output/password-type.graphql @@ -104,7 +104,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/searchables-references.graphql b/graphql/schema/testdata/schemagen/output/searchables-references.graphql index 99423bfe8e7..4be33ffbc92 100755 --- a/graphql/schema/testdata/schemagen/output/searchables-references.graphql +++ b/graphql/schema/testdata/schemagen/output/searchables-references.graphql @@ -113,7 +113,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/searchables.graphql b/graphql/schema/testdata/schemagen/output/searchables.graphql index f8a524fde2f..8b19d4e00f1 100755 --- a/graphql/schema/testdata/schemagen/output/searchables.graphql +++ b/graphql/schema/testdata/schemagen/output/searchables.graphql @@ -132,7 +132,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql b/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql index 60e184c0efc..735580f4551 100755 --- a/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql +++ b/graphql/schema/testdata/schemagen/output/single-type-with-enum.graphql @@ -112,7 +112,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/single-type.graphql b/graphql/schema/testdata/schemagen/output/single-type.graphql index 7a4dee5f713..b662c2667a0 100755 --- a/graphql/schema/testdata/schemagen/output/single-type.graphql +++ b/graphql/schema/testdata/schemagen/output/single-type.graphql @@ -107,7 +107,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql b/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql index e873d0b7c45..471bdd51020 100755 --- a/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql +++ b/graphql/schema/testdata/schemagen/output/type-implements-multiple-interfaces.graphql @@ -119,7 +119,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/type-reference.graphql b/graphql/schema/testdata/schemagen/output/type-reference.graphql index e47e71dac15..41efdb24ec9 100755 --- a/graphql/schema/testdata/schemagen/output/type-reference.graphql +++ b/graphql/schema/testdata/schemagen/output/type-reference.graphql @@ -111,7 +111,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/type-with-arguments-on-field.graphql b/graphql/schema/testdata/schemagen/output/type-with-arguments-on-field.graphql index 1589af200d6..b0df3e98ca5 100644 --- a/graphql/schema/testdata/schemagen/output/type-with-arguments-on-field.graphql +++ b/graphql/schema/testdata/schemagen/output/type-with-arguments-on-field.graphql @@ -112,7 +112,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/type-with-custom-field-on-dgraph-type.graphql b/graphql/schema/testdata/schemagen/output/type-with-custom-field-on-dgraph-type.graphql index 78976feb73a..bf459dfef04 100644 --- a/graphql/schema/testdata/schemagen/output/type-with-custom-field-on-dgraph-type.graphql +++ b/graphql/schema/testdata/schemagen/output/type-with-custom-field-on-dgraph-type.graphql @@ -111,7 +111,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/type-with-custom-fields-on-remote-type.graphql b/graphql/schema/testdata/schemagen/output/type-with-custom-fields-on-remote-type.graphql index fc81c3dfed5..49ac78144a3 100644 --- a/graphql/schema/testdata/schemagen/output/type-with-custom-fields-on-remote-type.graphql +++ b/graphql/schema/testdata/schemagen/output/type-with-custom-fields-on-remote-type.graphql @@ -111,7 +111,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql b/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql index 118bcd1b3e6..714c54fdc74 100644 --- a/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql +++ b/graphql/schema/testdata/schemagen/output/type-without-orderables.graphql @@ -106,7 +106,7 @@ directive @auth( update: AuthRule, delete:AuthRule) on OBJECT directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION -directive @remote on OBJECT | INTERFACE +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM directive @cascade(fields: [String]) on FIELD directive @lambda on FIELD_DEFINITION diff --git a/graphql/schema/testdata/schemagen/output/union.graphql b/graphql/schema/testdata/schemagen/output/union.graphql new file mode 100644 index 00000000000..c448f0fd220 --- /dev/null +++ b/graphql/schema/testdata/schemagen/output/union.graphql @@ -0,0 +1,624 @@ +####################### +# Input Schema +####################### + +interface Character { + id: ID! + name: String! @search(by: [exact]) + friends(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + enemyOf(filter: ResidentFilter): Resident + appearsIn(first: Int, offset: Int): [Episode!]! @search +} + +type Human implements Character { + id: ID! + name: String! @search(by: [exact]) + friends(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + enemyOf(filter: ResidentFilter): Resident + appearsIn(first: Int, offset: Int): [Episode!]! @search + starships(filter: StarshipFilter, order: StarshipOrder, first: Int, offset: Int): [Starship] + totalCredits: Int +} + +type Droid implements Character { + id: ID! + name: String! @search(by: [exact]) + friends(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + enemyOf(filter: ResidentFilter): Resident + appearsIn(first: Int, offset: Int): [Episode!]! @search + primaryFunction: String +} + +enum Episode { + NEWHOPE + EMPIRE + JEDI +} + +type Starship { + id: ID! + name: String! @search(by: [term]) + length: Float +} + +union Resident = Human | Droid | Starship + +union Tool @remote = Droid | Starship + +type Planet { + id: ID! + name: String! + residents(filter: ResidentFilter, first: Int, offset: Int): [Resident!] @dgraph(pred: "residents") + bestTool: Tool @custom(http: {url:"http://mock:8888/tool/$id",method:"GET"}) +} + +####################### +# Extended Definitions +####################### + +""" +The Int64 scalar type represents a signed 64‐bit numeric non‐fractional value. +Int64 can represent values in range [-(2^63),(2^63 - 1)]. +""" +scalar Int64 + +""" +The DateTime scalar type represents date and time as a string in RFC3339 format. +For example: "1985-04-12T23:20:50.52Z" represents 20 minutes and 50.52 seconds after the 23rd hour of April 12th, 1985 in UTC. +""" +scalar DateTime + +enum DgraphIndex { + int + int64 + float + bool + hash + exact + term + fulltext + trigram + regexp + year + month + day + hour + geo +} + +input AuthRule { + and: [AuthRule] + or: [AuthRule] + not: AuthRule + rule: String +} + +enum HTTPMethod { + GET + POST + PUT + PATCH + DELETE +} + +enum Mode { + BATCH + SINGLE +} + +input CustomHTTP { + url: String! + method: HTTPMethod! + body: String + graphql: String + mode: Mode + forwardHeaders: [String!] + secretHeaders: [String!] + introspectionHeaders: [String!] + skipIntrospection: Boolean +} + +type Point { + longitude: Float! + latitude: Float! +} + +input PointRef { + longitude: Float! + latitude: Float! +} + +input NearFilter { + distance: Float! + coordinate: PointRef! +} + +input PointGeoFilter { + near: NearFilter! +} + +directive @hasInverse(field: String!) on FIELD_DEFINITION +directive @search(by: [DgraphIndex!]) on FIELD_DEFINITION +directive @dgraph(type: String, pred: String) on OBJECT | INTERFACE | FIELD_DEFINITION +directive @id on FIELD_DEFINITION +directive @withSubscription on OBJECT | INTERFACE +directive @secret(field: String!, pred: String) on OBJECT | INTERFACE +directive @auth( + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete:AuthRule) on OBJECT +directive @custom(http: CustomHTTP, dql: String) on FIELD_DEFINITION +directive @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM +directive @cascade(fields: [String]) on FIELD +directive @lambda on FIELD_DEFINITION + +input IntFilter { + eq: Int + le: Int + lt: Int + ge: Int + gt: Int +} + +input Int64Filter { + eq: Int64 + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 +} + +input FloatFilter { + eq: Float + le: Float + lt: Float + ge: Float + gt: Float +} + +input DateTimeFilter { + eq: DateTime + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime +} + +input StringTermFilter { + allofterms: String + anyofterms: String +} + +input StringRegExpFilter { + regexp: String +} + +input StringFullTextFilter { + alloftext: String + anyoftext: String +} + +input StringExactFilter { + eq: String + in: [String] + le: String + lt: String + ge: String + gt: String +} + +input StringHashFilter { + eq: String + in: [String] +} + +####################### +# Generated Types +####################### + +type AddDroidPayload { + droid(filter: DroidFilter, order: DroidOrder, first: Int, offset: Int): [Droid] + numUids: Int +} + +type AddHumanPayload { + human(filter: HumanFilter, order: HumanOrder, first: Int, offset: Int): [Human] + numUids: Int +} + +type AddPlanetPayload { + planet(filter: PlanetFilter, order: PlanetOrder, first: Int, offset: Int): [Planet] + numUids: Int +} + +type AddStarshipPayload { + starship(filter: StarshipFilter, order: StarshipOrder, first: Int, offset: Int): [Starship] + numUids: Int +} + +type DeleteCharacterPayload { + character(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + msg: String + numUids: Int +} + +type DeleteDroidPayload { + droid(filter: DroidFilter, order: DroidOrder, first: Int, offset: Int): [Droid] + msg: String + numUids: Int +} + +type DeleteHumanPayload { + human(filter: HumanFilter, order: HumanOrder, first: Int, offset: Int): [Human] + msg: String + numUids: Int +} + +type DeletePlanetPayload { + planet(filter: PlanetFilter, order: PlanetOrder, first: Int, offset: Int): [Planet] + msg: String + numUids: Int +} + +type DeleteStarshipPayload { + starship(filter: StarshipFilter, order: StarshipOrder, first: Int, offset: Int): [Starship] + msg: String + numUids: Int +} + +type UpdateCharacterPayload { + character(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + numUids: Int +} + +type UpdateDroidPayload { + droid(filter: DroidFilter, order: DroidOrder, first: Int, offset: Int): [Droid] + numUids: Int +} + +type UpdateHumanPayload { + human(filter: HumanFilter, order: HumanOrder, first: Int, offset: Int): [Human] + numUids: Int +} + +type UpdatePlanetPayload { + planet(filter: PlanetFilter, order: PlanetOrder, first: Int, offset: Int): [Planet] + numUids: Int +} + +type UpdateStarshipPayload { + starship(filter: StarshipFilter, order: StarshipOrder, first: Int, offset: Int): [Starship] + numUids: Int +} + +####################### +# Generated Enums +####################### + +enum CharacterHasFilter { + name + friends + enemyOf + appearsIn +} + +enum CharacterOrderable { + name +} + +enum DroidHasFilter { + name + friends + enemyOf + appearsIn + primaryFunction +} + +enum DroidOrderable { + name + primaryFunction +} + +enum HumanHasFilter { + name + friends + enemyOf + appearsIn + starships + totalCredits +} + +enum HumanOrderable { + name + totalCredits +} + +enum PlanetHasFilter { + name + residents +} + +enum PlanetOrderable { + name +} + +enum ResidentType { + Human + Droid + Starship +} + +enum StarshipHasFilter { + name + length +} + +enum StarshipOrderable { + name + length +} + +####################### +# Generated Inputs +####################### + +input AddDroidInput { + name: String! + friends: [CharacterRef] + enemyOf: ResidentRef + appearsIn: [Episode!]! + primaryFunction: String +} + +input AddHumanInput { + name: String! + friends: [CharacterRef] + enemyOf: ResidentRef + appearsIn: [Episode!]! + starships: [StarshipRef] + totalCredits: Int +} + +input AddPlanetInput { + name: String! + residents: [ResidentRef!] +} + +input AddStarshipInput { + name: String! + length: Float +} + +input CharacterFilter { + id: [ID!] + name: StringExactFilter + appearsIn: Episode_hash + has: CharacterHasFilter + and: CharacterFilter + or: CharacterFilter + not: CharacterFilter +} + +input CharacterOrder { + asc: CharacterOrderable + desc: CharacterOrderable + then: CharacterOrder +} + +input CharacterPatch { + name: String + friends: [CharacterRef] + enemyOf: ResidentRef + appearsIn: [Episode!] +} + +input CharacterRef { + id: ID! +} + +input DroidFilter { + id: [ID!] + name: StringExactFilter + appearsIn: Episode_hash + has: DroidHasFilter + and: DroidFilter + or: DroidFilter + not: DroidFilter +} + +input DroidOrder { + asc: DroidOrderable + desc: DroidOrderable + then: DroidOrder +} + +input DroidPatch { + name: String + friends: [CharacterRef] + enemyOf: ResidentRef + appearsIn: [Episode!] + primaryFunction: String +} + +input DroidRef { + id: ID + name: String + friends: [CharacterRef] + enemyOf: ResidentRef + appearsIn: [Episode!] + primaryFunction: String +} + +input Episode_hash { + eq: [Episode!]! + in: [Episode!]! +} + +input HumanFilter { + id: [ID!] + name: StringExactFilter + appearsIn: Episode_hash + has: HumanHasFilter + and: HumanFilter + or: HumanFilter + not: HumanFilter +} + +input HumanOrder { + asc: HumanOrderable + desc: HumanOrderable + then: HumanOrder +} + +input HumanPatch { + name: String + friends: [CharacterRef] + enemyOf: ResidentRef + appearsIn: [Episode!] + starships: [StarshipRef] + totalCredits: Int +} + +input HumanRef { + id: ID + name: String + friends: [CharacterRef] + enemyOf: ResidentRef + appearsIn: [Episode!] + starships: [StarshipRef] + totalCredits: Int +} + +input PlanetFilter { + id: [ID!] + has: PlanetHasFilter + and: PlanetFilter + or: PlanetFilter + not: PlanetFilter +} + +input PlanetOrder { + asc: PlanetOrderable + desc: PlanetOrderable + then: PlanetOrder +} + +input PlanetPatch { + name: String + residents: [ResidentRef!] +} + +input PlanetRef { + id: ID + name: String + residents: [ResidentRef!] +} + +input ResidentFilter { + memberTypes: [ResidentType!] + humanFilter: HumanFilter + droidFilter: DroidFilter + starshipFilter: StarshipFilter +} + +input ResidentRef { + humanRef: HumanRef + droidRef: DroidRef + starshipRef: StarshipRef +} + +input StarshipFilter { + id: [ID!] + name: StringTermFilter + has: StarshipHasFilter + and: StarshipFilter + or: StarshipFilter + not: StarshipFilter +} + +input StarshipOrder { + asc: StarshipOrderable + desc: StarshipOrderable + then: StarshipOrder +} + +input StarshipPatch { + name: String + length: Float +} + +input StarshipRef { + id: ID + name: String + length: Float +} + +input UpdateCharacterInput { + filter: CharacterFilter! + set: CharacterPatch + remove: CharacterPatch +} + +input UpdateDroidInput { + filter: DroidFilter! + set: DroidPatch + remove: DroidPatch +} + +input UpdateHumanInput { + filter: HumanFilter! + set: HumanPatch + remove: HumanPatch +} + +input UpdatePlanetInput { + filter: PlanetFilter! + set: PlanetPatch + remove: PlanetPatch +} + +input UpdateStarshipInput { + filter: StarshipFilter! + set: StarshipPatch + remove: StarshipPatch +} + +####################### +# Generated Query +####################### + +type Query { + getCharacter(id: ID!): Character + queryCharacter(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + getHuman(id: ID!): Human + queryHuman(filter: HumanFilter, order: HumanOrder, first: Int, offset: Int): [Human] + getDroid(id: ID!): Droid + queryDroid(filter: DroidFilter, order: DroidOrder, first: Int, offset: Int): [Droid] + getStarship(id: ID!): Starship + queryStarship(filter: StarshipFilter, order: StarshipOrder, first: Int, offset: Int): [Starship] + getPlanet(id: ID!): Planet + queryPlanet(filter: PlanetFilter, order: PlanetOrder, first: Int, offset: Int): [Planet] +} + +####################### +# Generated Mutations +####################### + +type Mutation { + updateCharacter(input: UpdateCharacterInput!): UpdateCharacterPayload + deleteCharacter(filter: CharacterFilter!): DeleteCharacterPayload + addHuman(input: [AddHumanInput!]!): AddHumanPayload + updateHuman(input: UpdateHumanInput!): UpdateHumanPayload + deleteHuman(filter: HumanFilter!): DeleteHumanPayload + addDroid(input: [AddDroidInput!]!): AddDroidPayload + updateDroid(input: UpdateDroidInput!): UpdateDroidPayload + deleteDroid(filter: DroidFilter!): DeleteDroidPayload + addStarship(input: [AddStarshipInput!]!): AddStarshipPayload + updateStarship(input: UpdateStarshipInput!): UpdateStarshipPayload + deleteStarship(filter: StarshipFilter!): DeleteStarshipPayload + addPlanet(input: [AddPlanetInput!]!): AddPlanetPayload + updatePlanet(input: UpdatePlanetInput!): UpdatePlanetPayload + deletePlanet(filter: PlanetFilter!): DeletePlanetPayload +} + diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index 35791a7067e..9a533116969 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -131,8 +131,8 @@ type Field interface { Location() x.Location DgraphPredicate() string Operation() Operation - // InterfaceType tells us whether this field represents a GraphQL Interface. - InterfaceType() bool + // AbstractType tells us whether this field represents a GraphQL Interface. + AbstractType() bool IncludeInterfaceField(types []interface{}) bool TypeName(dgraphTypes []interface{}) string GetObjectName() string @@ -173,6 +173,10 @@ type Type interface { DgraphName() string DgraphPredicate(fld string) string Nullable() bool + // true if this is a union type + IsUnion() bool + // returns a list of member types for this union + UnionMembers([]interface{}) []Type ListType() Type Interfaces() []string EnsureNonNulls(map[string]interface{}, string) error @@ -194,6 +198,7 @@ type FieldDefinition interface { IsID() bool HasIDDirective() bool Inverse() FieldDefinition + WithMemberType(string) FieldDefinition // TODO - It might be possible to get rid of ForwardEdge and just use Inverse() always. ForwardEdge() FieldDefinition } @@ -559,15 +564,10 @@ func repeatedFieldMappings(s *ast.Schema, dgPreds map[string]map[string]string) repeatedFieldNames := make(map[string]bool) for _, typ := range s.Types { - if typ.Kind != ast.Interface { + if !isAbstractKind(typ.Kind) { continue } - interfaceFields := make(map[string]bool) - for _, field := range typ.Fields { - interfaceFields[field.Name] = true - } - type fieldInfo struct { dgPred string repeated bool @@ -580,12 +580,14 @@ func repeatedFieldMappings(s *ast.Schema, dgPreds map[string]map[string]string) // ignore this field if it was inherited from the common interface or is of ID type. // We ignore ID type fields too, because they map only to uid in dgraph and can't // map to two different predicates. - if interfaceFields[field.Name] || field.Type.Name() == IDType { + if field.Type.Name() == IDType { continue } // if we find a field with same name from types implementing a common interface // and its DgraphPredicate is different than what was previously encountered, then - // we mark it as repeated field, so that queries will rewrite it with correct alias + // we mark it as repeated field, so that queries will rewrite it with correct alias. + // For fields, which these types have implemented from a common interface, their + // DgraphPredicate will be same, so they won't be marked as repeated. dgPred := typPreds[field.Name] if fInfo, ok := repeatedFieldsInTypesWithCommonAncestor[field.Name]; ok && fInfo. dgPred != dgPred { @@ -1065,8 +1067,12 @@ func (f *field) Type() Type { } } -func (f *field) InterfaceType() bool { - return f.op.inSchema.schema.Types[f.field.Definition.Type.Name()].Kind == ast.Interface +func isAbstractKind(kind ast.DefinitionKind) bool { + return kind == ast.Interface || kind == ast.Union +} + +func (f *field) AbstractType() bool { + return isAbstractKind(f.op.inSchema.schema.Types[f.field.Definition.Type.Name()].Kind) } func (f *field) GetObjectName() string { @@ -1252,11 +1258,15 @@ func (f *field) IncludeInterfaceField(dgraphTypes []interface{}) bool { } for _, origTyp := range f.op.inSchema.typeNameAst[styp] { if origTyp.Kind == ast.Object { - // If the field is from an interface implemented by this object, - // and was fetched not because of a fragment on this object, - // but because of a fragment on some other object, then we don't need to include it. + // For fields coming from fragments inside an abstract type, there are two cases: + // * If the field is from an interface implemented by this object, and was fetched + // not because of a fragment on this object, but because of a fragment on some + // other object, then we don't need to include it. + // * If the field was fetched because of a fragment on an interface, and that + // interface is not one of the interfaces implemented by this object, then we + // don't need to include it. fragType, ok := f.op.interfaceImplFragFields[f.field] - if ok && fragType != origTyp.Name { + if ok && fragType != origTyp.Name && !x.HasString(origTyp.Interfaces, fragType) { return false } @@ -1421,8 +1431,8 @@ func (q *query) DgraphPredicate() string { return (*field)(q).DgraphPredicate() } -func (q *query) InterfaceType() bool { - return (*field)(q).InterfaceType() +func (q *query) AbstractType() bool { + return (*field)(q).AbstractType() } func (q *query) TypeName(dgraphTypes []interface{}) string { @@ -1485,8 +1495,8 @@ func (m *mutation) Type() Type { return (*field)(m).Type() } -func (m *mutation) InterfaceType() bool { - return (*field)(m).InterfaceType() +func (m *mutation) AbstractType() bool { + return (*field)(m).AbstractType() } func (m *mutation) XIDArg() string { @@ -1703,6 +1713,23 @@ func (fd *fieldDefinition) Inverse() FieldDefinition { } } +func (fd *fieldDefinition) WithMemberType(memberType string) FieldDefinition { + // just need to return a copy of this fieldDefinition with type set to memberType + return &fieldDefinition{ + fieldDef: &ast.FieldDefinition{ + Name: fd.fieldDef.Name, + Arguments: fd.fieldDef.Arguments, + DefaultValue: fd.fieldDef.DefaultValue, + Type: &ast.Type{NamedType: memberType}, + Directives: fd.fieldDef.Directives, + Position: fd.fieldDef.Position, + }, + inSchema: fd.inSchema, + dgraphPredicate: fd.dgraphPredicate, + parentType: fd.parentType, + } +} + // ForwardEdge gets the field definition for a forward edge if this field is a reverse edge // i.e. if it has a dgraph directive like // @dgraph(name: "~movies") @@ -1773,6 +1800,34 @@ func (t *astType) Nullable() bool { return !t.typ.NonNull } +func (t *astType) IsUnion() bool { + return t.inSchema.schema.Types[t.typ.Name()].Kind == ast.Union +} + +func (t *astType) UnionMembers(memberTypesList []interface{}) []Type { + var memberTypes []Type + if (memberTypesList) == nil { + // if no specific members were requested, find out all the members of this union + for _, typName := range t.inSchema.schema.Types[t.typ.Name()].Types { + memberTypes = append(memberTypes, &astType{ + typ: &ast.Type{NamedType: typName}, + inSchema: t.inSchema, + dgraphPredicate: t.dgraphPredicate, + }) + } + } else { + // else return wrapper for only the members which were requested + for _, typName := range memberTypesList { + memberTypes = append(memberTypes, &astType{ + typ: &ast.Type{NamedType: typName.(string)}, + inSchema: t.inSchema, + dgraphPredicate: t.dgraphPredicate, + }) + } + } + return memberTypes +} + func (t *astType) ListType() Type { if t.typ == nil || t.typ.Elem == nil { return nil