diff --git a/graphql/admin/admin.go b/graphql/admin/admin.go index c3bd734bc16..34449e5dc05 100644 --- a/graphql/admin/admin.go +++ b/graphql/admin/admin.go @@ -378,7 +378,7 @@ var ( ) func SchemaValidate(sch string) error { - schHandler, err := schema.NewHandler(sch, true) + schHandler, err := schema.NewHandler(sch, true, false) if err != nil { return err } @@ -638,7 +638,7 @@ func getCurrentGraphQLSchema() (*gqlSchema, error) { } func generateGQLSchema(sch *gqlSchema) (schema.Schema, error) { - schHandler, err := schema.NewHandler(sch.Schema, false) + schHandler, err := schema.NewHandler(sch.Schema, false, false) if err != nil { return nil, err } @@ -836,6 +836,7 @@ func resolverFactoryWithErrorMsg(msg string) resolve.ResolverFactory { return resolve.NewResolverFactory(qErr, mErr) } +// Todo(Minhaj): Fetch NewHandler for service query only once func (as *adminServer) resetSchema(gqlSchema schema.Schema) { // set status as updating schema mainHealthStore.updatingSchema() @@ -849,6 +850,26 @@ func (as *adminServer) resetSchema(gqlSchema schema.Schema) { } else { resolverFactory = resolverFactoryWithErrorMsg(errResolverNotFound). WithConventionResolvers(gqlSchema, as.fns) + // If the schema is a Federated Schema then attach "_service" resolver + if gqlSchema.IsFederated() { + resolverFactory.WithQueryResolver("_service", func(s schema.Query) resolve.QueryResolver { + return resolve.QueryResolverFunc(func(ctx context.Context, query schema.Query) *resolve.Resolved { + as.mux.RLock() + defer as.mux.RUnlock() + sch := as.schema.Schema + handler, err := schema.NewHandler(sch, false, true) + if err != nil { + return resolve.EmptyResult(query, err) + } + data := handler.GQLSchemaWithoutApolloExtras() + return &resolve.Resolved{ + Data: map[string]interface{}{"_service": map[string]interface{}{"sdl": data}}, + Field: query, + } + }) + }) + } + if as.withIntrospection { resolverFactory.WithSchemaIntrospection() } diff --git a/graphql/admin/schema.go b/graphql/admin/schema.go index 4dd6af8c2d8..0c160fbcd65 100644 --- a/graphql/admin/schema.go +++ b/graphql/admin/schema.go @@ -57,7 +57,7 @@ func (usr *updateSchemaResolver) Resolve(ctx context.Context, m schema.Mutation) // We just need to validate the schema. Schema is later set in `resetSchema()` when the schema // is returned from badger. - schHandler, err := schema.NewHandler(input.Set.Schema, true) + schHandler, err := schema.NewHandler(input.Set.Schema, true, false) if err != nil { return resolve.EmptyResult(m, err), false } diff --git a/graphql/dgraph/graphquery.go b/graphql/dgraph/graphquery.go index 5f5e1983857..94aaac698b6 100644 --- a/graphql/dgraph/graphquery.go +++ b/graphql/dgraph/graphquery.go @@ -137,7 +137,8 @@ func writeUIDFunc(b *strings.Builder, uids []uint64, args []gql.Arg) { // writeRoot writes the root function as well as any ordering and paging // specified in q. // -// Only uid(0x123, 0x124) and type(...) functions are supported at root. +// Only uid(0x123, 0x124), type(...) and eq(Type.Predicate, ...) functions are supported at root. +// Multiple arguments for `eq` filter will be required in case of resolving `entities` query. func writeRoot(b *strings.Builder, q *gql.GraphQuery) { if q.Func == nil { return @@ -149,9 +150,10 @@ func writeRoot(b *strings.Builder, q *gql.GraphQuery) { writeUIDFunc(b, q.Func.UID, q.Func.Args) case q.Func.Name == "type" && len(q.Func.Args) == 1: x.Check2(b.WriteString(fmt.Sprintf("(func: type(%s)", q.Func.Args[0].Value))) - case q.Func.Name == "eq" && len(q.Func.Args) == 2: - x.Check2(b.WriteString(fmt.Sprintf("(func: eq(%s, %s)", q.Func.Args[0].Value, - q.Func.Args[1].Value))) + case q.Func.Name == "eq": + x.Check2(b.WriteString("(func: eq(")) + writeFilterArguments(b, q.Func.Args) + x.Check2(b.WriteRune(')')) } writeOrderAndPage(b, q, true) x.Check2(b.WriteRune(')')) diff --git a/graphql/e2e/auth/schema.graphql b/graphql/e2e/auth/schema.graphql index 883d5636ea2..d79e3ff91ec 100644 --- a/graphql/e2e/auth/schema.graphql +++ b/graphql/e2e/auth/schema.graphql @@ -775,3 +775,31 @@ type Book @auth( name: String! desc: String! } + + +type Mission @key(fields: "id") @auth( + query:{ rule: """ + query($USER: String!) { + queryMission(filter: { supervisorName: {eq: $USER} } ) { + id + } + }""" } +){ + id: String! @id + crew: [Astronaut] + supervisorName: String @search(by: [exact]) + designation: String! + startDate: String + endDate: String +} + +type Astronaut @key(fields: "id") @extends @auth( + query: { rule: "{$ROLE: { eq: \"admin\" } }"}, + add: { rule: "{$USER: { eq: \"foo\" } }"}, + delete: { rule: "{$USER: { eq: \"foo\" } }"}, + update: { rule: "{$USER: { eq: \"foo\" } }"} +){ + id: ID! @external + missions: [Mission] +} + diff --git a/graphql/e2e/common/common.go b/graphql/e2e/common/common.go index f7a4de6b09a..ed003e8ea3b 100644 --- a/graphql/e2e/common/common.go +++ b/graphql/e2e/common/common.go @@ -137,6 +137,11 @@ type country struct { States []*state `json:"states,omitempty"` } +type mission struct { + ID string `json:"id,omitempty"` + Designation string `json:"designation,omitempty"` +} + type author struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` @@ -630,6 +635,7 @@ func RunAll(t *testing.T) { t.Run("query only typename", queryOnlyTypename) t.Run("query nested only typename", querynestedOnlyTypename) t.Run("test onlytypename for interface types", onlytypenameForInterface) + t.Run("entitites Query on extended type", entitiesQuery) t.Run("get state by xid", getStateByXid) t.Run("get state without args", getStateWithoutArgs) @@ -713,6 +719,8 @@ func RunAll(t *testing.T) { t.Run("mutation id directive with int", idDirectiveWithIntMutation) t.Run("mutation id directive with int64", idDirectiveWithInt64Mutation) t.Run("mutation id directive with float", idDirectiveWithFloatMutation) + t.Run("add mutation on extended type with field of ID type as key field", addMutationOnExtendedTypeWithIDasKeyField) + t.Run("add mutation with deep extended type objects", addMutationWithDeepExtendedTypeObjects) // error tests t.Run("graphql completion on", graphQLCompletionOn) diff --git a/graphql/e2e/common/mutation.go b/graphql/e2e/common/mutation.go index 02fb8ad6f6e..d59e67f1e18 100644 --- a/graphql/e2e/common/mutation.go +++ b/graphql/e2e/common/mutation.go @@ -4674,3 +4674,149 @@ func idDirectiveWithFloatMutation(t *testing.T) { DeleteGqlType(t, "Section", map[string]interface{}{}, 4, nil) } + +func addMutationWithDeepExtendedTypeObjects(t *testing.T) { + varMap1 := map[string]interface{}{ + "missionId": "Mission1", + "astronautId": "Astronaut1", + "des": "Apollo1", + } + addMissionParams := &GraphQLParams{ + Query: `mutation addMission($missionId: String!, $astronautId: ID!, $des: String!) { + addMission(input: [{id: $missionId, designation: $des, crew: [{id: $astronautId}]}]) { + mission{ + id + crew { + id + missions(order: {asc: id}){ + id + } + } + } + } + } + `, + Variables: varMap1, + } + gqlResponse := addMissionParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + expectedJSON := `{ + "addMission": { + "mission": [ + { + "id": "Mission1", + "crew": [ + { + "id": "Astronaut1", + "missions": [ + { + "id": "Mission1" + } + ] + } + ] + } + ] + } + }` + testutil.CompareJSON(t, expectedJSON, string(gqlResponse.Data)) + + varMap2 := map[string]interface{}{ + "missionId": "Mission2", + "astronautId": "Astronaut1", + "des": "Apollo2", + } + addMissionParams.Variables = varMap2 + + gqlResponse1 := addMissionParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + expectedJSON = `{ + "addMission": { + "mission": [ + { + "id": "Mission2", + "crew": [ + { + "id": "Astronaut1", + "missions": [ + { + "id": "Mission1" + }, + { + "id": "Mission2" + } + ] + } + ] + } + ] + } + }` + testutil.CompareJSON(t, expectedJSON, string(gqlResponse1.Data)) + + astronautDeleteFilter := map[string]interface{}{"id": []string{"Astronaut1"}} + DeleteGqlType(t, "Astronaut", astronautDeleteFilter, 1, nil) + + missionDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"Mission1", "Mission2"}}} + DeleteGqlType(t, "Mission", missionDeleteFilter, 2, nil) +} + +func addMutationOnExtendedTypeWithIDasKeyField(t *testing.T) { + addAstronautParams := &GraphQLParams{ + Query: `mutation addAstronaut($id1: ID!, $missionId1: String!, $id2: ID!, $missionId2: String! ) { + addAstronaut(input: [{id: $id1, missions: [{id: $missionId1, designation: "Apollo1"}]}, {id: $id2, missions: [{id: $missionId2, designation: "Apollo2"}]}]) { + astronaut(order: {asc: id}){ + id + missions { + id + designation + } + } + } + }`, + Variables: map[string]interface{}{ + "id1": "Astronaut1", + "missionId1": "Mission1", + "id2": "Astronaut2", + "missionId2": "Mission2", + }, + } + + gqlResponse := addAstronautParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + expectedJSON := `{ + "addAstronaut": { + "astronaut": [ + { + "id": "Astronaut1", + "missions": [ + { + "id": "Mission1", + "designation": "Apollo1" + } + ] + }, + { + "id": "Astronaut2", + "missions": [ + { + "id": "Mission2", + "designation": "Apollo2" + } + ] + } + ] + } + }` + + testutil.CompareJSON(t, expectedJSON, string(gqlResponse.Data)) + + astronautDeleteFilter := map[string]interface{}{"id": []string{"Astronaut1", "Astronaut2"}} + DeleteGqlType(t, "Astronaut", astronautDeleteFilter, 2, nil) + + missionDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"Mission1", "Mission2"}}} + DeleteGqlType(t, "Mission", missionDeleteFilter, 2, nil) +} diff --git a/graphql/e2e/common/query.go b/graphql/e2e/common/query.go index a5bd298ac63..c015e2dfca5 100644 --- a/graphql/e2e/common/query.go +++ b/graphql/e2e/common/query.go @@ -19,7 +19,6 @@ package common import ( "encoding/json" "fmt" - "github.com/spf13/cast" "io/ioutil" "math/rand" "net/http" @@ -29,6 +28,8 @@ import ( "testing" "time" + "github.com/spf13/cast" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/dgraph-io/dgraph/graphql/schema" @@ -374,6 +375,71 @@ func allPosts(t *testing.T) []*post { return result.QueryPost } +func entitiesQuery(t *testing.T) { + addSpaceShipParams := &GraphQLParams{ + Query: `mutation addSpaceShip($id1: String!, $missionId1: String! ) { + addSpaceShip(input: [{id: $id1, missions: [{id: $missionId1, designation: "Apollo1"}]} ]) { + spaceShip { + id + missions { + id + designation + } + } + } + }`, + Variables: map[string]interface{}{ + "id1": "SpaceShip1", + "missionId1": "Mission1", + }, + } + + gqlResponse := addSpaceShipParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, gqlResponse) + + entitiesQueryParams := &GraphQLParams{ + Query: `query _entities($typeName: String!, $id1: String!){ + _entities(representations: [{__typename: $typeName, id: $id1}]) { + ... on SpaceShip { + missions(order: {asc: id}){ + id + designation + } + } + } + }`, + Variables: map[string]interface{}{ + "typeName": "SpaceShip", + "id1": "SpaceShip1", + }, + } + + entitiesResp := entitiesQueryParams.ExecuteAsPost(t, GraphqlURL) + RequireNoGQLErrors(t, entitiesResp) + + expectedJSON := `{ + "_entities": [ + { + "missions": [ + { + "id": "Mission1", + "designation": "Apollo1" + } + ] + } + ] + }` + + testutil.CompareJSON(t, expectedJSON, string(entitiesResp.Data)) + + spaceShipDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"SpaceShip1"}}} + DeleteGqlType(t, "SpaceShip", spaceShipDeleteFilter, 1, nil) + + missionDeleteFilter := map[string]interface{}{"id": map[string]interface{}{"in": []string{"Mission1"}}} + DeleteGqlType(t, "Mission", missionDeleteFilter, 1, nil) + +} + func inFilterOnString(t *testing.T) { addStateParams := &GraphQLParams{ Query: `mutation addState($name1: String!, $code1: String!, $name2: String!, $code2: String! ) { diff --git a/graphql/e2e/directives/schema.graphql b/graphql/e2e/directives/schema.graphql index dccee56f481..7a1774398ff 100644 --- a/graphql/e2e/directives/schema.graphql +++ b/graphql/e2e/directives/schema.graphql @@ -302,3 +302,24 @@ type Section { name: String! chapterId: Int! @search } + +# test for entities resolver + +type Mission @key(fields: "id") { + id: String! @id + crew: [Astronaut] @hasInverse(field: missions) + spaceShip: [SpaceShip] + designation: String! + startDate: String + endDate: String +} + +type Astronaut @key(fields: "id") @extends { + id: ID! @external + missions: [Mission] +} + +type SpaceShip @key(fields: "id") @extends { + id: String! @id @external + missions: [Mission] +} \ No newline at end of file diff --git a/graphql/e2e/directives/schema_response.json b/graphql/e2e/directives/schema_response.json index daf9b4c6c1d..69c271f53ba 100644 --- a/graphql/e2e/directives/schema_response.json +++ b/graphql/e2e/directives/schema_response.json @@ -8,6 +8,20 @@ "hash" ] }, + { + "predicate": "Astronaut.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Astronaut.missions", + "type": "uid", + "list": true + }, { "predicate": "Category.name", "type": "string" @@ -102,6 +116,37 @@ "type": "uid", "list": true }, + { + "predicate": "Mission.crew", + "type": "uid", + "list": true + }, + { + "predicate": "Mission.designation", + "type": "string" + }, + { + "predicate": "Mission.endDate", + "type": "string" + }, + { + "predicate": "Mission.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Mission.spaceShip", + "type": "uid", + "list": true + }, + { + "predicate": "Mission.startDate", + "type": "string" + }, { "predicate": "Movie.name", "type": "string" @@ -422,6 +467,20 @@ "predicate": "roboDroid.primaryFunction", "type": "string" }, + { + "predicate": "SpaceShip.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "SpaceShip.missions", + "type": "uid", + "list": true + }, { "predicate": "star.ship.length", "type": "float" @@ -570,6 +629,17 @@ ], "name": "Animal" }, + { + "fields": [ + { + "name": "Astronaut.id" + }, + { + "name": "Astronaut.missions" + } + ], + "name": "Astronaut" + }, { "fields": [ { @@ -687,6 +757,29 @@ ], "name": "Message" }, + { + "fields": [ + { + "name": "Mission.id" + }, + { + "name": "Mission.crew" + }, + { + "name": "Mission.spaceShip" + }, + { + "name": "Mission.designation" + }, + { + "name": "Mission.startDate" + }, + { + "name": "Mission.endDate" + } + ], + "name": "Mission" + }, { "fields": [ { @@ -758,6 +851,17 @@ ], "name": "Post1" }, + { + "fields": [ + { + "name": "SpaceShip.id" + }, + { + "name": "SpaceShip.missions" + } + ], + "name": "SpaceShip" + }, { "fields": [ { diff --git a/graphql/e2e/normal/schema.graphql b/graphql/e2e/normal/schema.graphql index d8f82300d4b..fc0332d6363 100644 --- a/graphql/e2e/normal/schema.graphql +++ b/graphql/e2e/normal/schema.graphql @@ -302,4 +302,25 @@ type Section { sectionId: Float! @id name: String! chapterId: Int! @search +} + +# test for entities resolver + +type Mission @key(fields: "id") { + id: String! @id + crew: [Astronaut] @hasInverse(field: missions) + spaceShip: [SpaceShip] + designation: String! + startDate: String + endDate: String +} + +type Astronaut @key(fields: "id") @extends { + id: ID! @external + missions: [Mission] +} + +type SpaceShip @key(fields: "id") @extends { + id: String! @id @external + missions: [Mission] } \ No newline at end of file diff --git a/graphql/e2e/normal/schema_response.json b/graphql/e2e/normal/schema_response.json index 414e59a8f8b..3b4523ecb97 100644 --- a/graphql/e2e/normal/schema_response.json +++ b/graphql/e2e/normal/schema_response.json @@ -8,6 +8,20 @@ "hash" ] }, + { + "predicate": "Astronaut.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Astronaut.missions", + "type": "uid", + "list": true + }, { "predicate": "Author.country", "type": "uid" @@ -179,6 +193,37 @@ "predicate": "Human.totalCredits", "type": "float" }, + { + "predicate": "Mission.crew", + "type": "uid", + "list": true + }, + { + "predicate": "Mission.designation", + "type": "string" + }, + { + "predicate": "Mission.endDate", + "type": "string" + }, + { + "predicate": "Mission.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "Mission.spaceShip", + "type": "uid", + "list": true + }, + { + "predicate": "Mission.startDate", + "type": "string" + }, { "predicate": "Movie.director", "type": "uid", @@ -321,6 +366,20 @@ ], "upsert": true }, + { + "predicate": "SpaceShip.id", + "type": "string", + "index": true, + "tokenizer": [ + "hash" + ], + "upsert": true + }, + { + "predicate": "SpaceShip.missions", + "type": "uid", + "list": true + }, { "predicate": "Starship.length", "type": "float" @@ -570,6 +629,17 @@ ], "name": "Animal" }, + { + "fields": [ + { + "name": "Astronaut.id" + }, + { + "name": "Astronaut.missions" + } + ], + "name": "Astronaut" + }, { "fields": [ { @@ -732,6 +802,29 @@ ], "name": "Human" }, + { + "fields": [ + { + "name": "Mission.id" + }, + { + "name": "Mission.crew" + }, + { + "name": "Mission.spaceShip" + }, + { + "name": "Mission.designation" + }, + { + "name": "Mission.startDate" + }, + { + "name": "Mission.endDate" + } + ], + "name": "Mission" + }, { "fields": [ { @@ -891,6 +984,17 @@ ], "name": "Post1" }, + { + "fields": [ + { + "name": "SpaceShip.id" + }, + { + "name": "SpaceShip.missions" + } + ], + "name": "SpaceShip" + }, { "fields": [ { diff --git a/graphql/e2e/schema/apollo_service_response.graphql b/graphql/e2e/schema/apollo_service_response.graphql new file mode 100644 index 00000000000..602544175c3 --- /dev/null +++ b/graphql/e2e/schema/apollo_service_response.graphql @@ -0,0 +1,527 @@ +####################### +# Input Schema +####################### + +type Mission { + id: ID! + crew: [Astronaut] + designation: String! + startDate: String + endDate: String +} + +type Astronaut @key(fields: "id") @extends { + id: ID! @external + missions(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + missionsAggregate(filter: MissionFilter): MissionAggregateResult +} + +type User @remote { + id: ID! + name: String! +} + +type Car { + id: ID! + name: String! +} + +####################### +# 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 + +input IntRange{ + min: Int! + max: Int! +} + +input FloatRange{ + min: Float! + max: Float! +} + +input Int64Range{ + min: Int64! + max: Int64! +} + +input DateTimeRange{ + min: DateTime! + max: DateTime! +} + +input StringRange{ + min: String! + max: String! +} + +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 + within: WithinFilter +} + +type PointList { + points: [Point!]! +} + +input PointListRef { + points: [PointRef!]! +} + +type Polygon { + coordinates: [PointList!]! +} + +input PolygonRef { + coordinates: [PointListRef!]! +} + +type MultiPolygon { + polygons: [Polygon!]! +} + +input MultiPolygonRef { + polygons: [PolygonRef!]! +} + +input WithinFilter { + polygon: PolygonRef! +} + +input ContainsFilter { + point: PointRef + polygon: PolygonRef +} + +input IntersectsFilter { + polygon: PolygonRef + multiPolygon: MultiPolygonRef +} + +input PolygonGeoFilter { + near: NearFilter + within: WithinFilter + contains: ContainsFilter + intersects: IntersectsFilter +} + +input GenerateQueryParams { + get: Boolean + query: Boolean + password: Boolean + aggregate: Boolean +} + +input GenerateMutationParams { + add: Boolean + update: Boolean + delete: Boolean +} + +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 @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM +directive @cascade(fields: [String]) on FIELD +directive @lambda on FIELD_DEFINITION +directive @cacheControl(maxAge: Int!) on QUERY + +input IntFilter { + eq: Int + in: [Int] + le: Int + lt: Int + ge: Int + gt: Int + between: IntRange +} + +input Int64Filter { + eq: Int64 + in: [Int64] + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 + between: Int64Range +} + +input FloatFilter { + eq: Float + in: [Float] + le: Float + lt: Float + ge: Float + gt: Float + between: FloatRange +} + +input DateTimeFilter { + eq: DateTime + in: [DateTime] + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime + between: DateTimeRange +} + +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 + between: StringRange +} + +input StringHashFilter { + eq: String + in: [String] +} + +####################### +# Generated Types +####################### + +type AddAstronautPayload { + astronaut(filter: AstronautFilter, order: AstronautOrder, first: Int, offset: Int): [Astronaut] + numUids: Int +} + +type AddCarPayload { + car(filter: CarFilter, order: CarOrder, first: Int, offset: Int): [Car] + numUids: Int +} + +type AddMissionPayload { + mission(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + numUids: Int +} + +type AstronautAggregateResult { + count: Int + idMin: ID + idMax: ID +} + +type CarAggregateResult { + count: Int + nameMin: String + nameMax: String +} + +type DeleteAstronautPayload { + astronaut(filter: AstronautFilter, order: AstronautOrder, first: Int, offset: Int): [Astronaut] + msg: String + numUids: Int +} + +type DeleteCarPayload { + car(filter: CarFilter, order: CarOrder, first: Int, offset: Int): [Car] + msg: String + numUids: Int +} + +type DeleteMissionPayload { + mission(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + msg: String + numUids: Int +} + +type MissionAggregateResult { + count: Int + designationMin: String + designationMax: String + startDateMin: String + startDateMax: String + endDateMin: String + endDateMax: String +} + +type UpdateAstronautPayload { + astronaut(filter: AstronautFilter, order: AstronautOrder, first: Int, offset: Int): [Astronaut] + numUids: Int +} + +type UpdateCarPayload { + car(filter: CarFilter, order: CarOrder, first: Int, offset: Int): [Car] + numUids: Int +} + +type UpdateMissionPayload { + mission(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + numUids: Int +} + +####################### +# Generated Enums +####################### + +enum AstronautHasFilter { + missions +} + +enum AstronautOrderable { + id +} + +enum CarHasFilter { + name +} + +enum CarOrderable { + name +} + +enum MissionHasFilter { + crew + designation + startDate + endDate +} + +enum MissionOrderable { + designation + startDate + endDate +} + +####################### +# Generated Inputs +####################### + +input AddAstronautInput { + id: ID! + missions: [MissionRef] +} + +input AddCarInput { + name: String! +} + +input AddMissionInput { + crew: [AstronautRef] + designation: String! + startDate: String + endDate: String +} + +input AstronautFilter { + id: [ID!] + has: AstronautHasFilter + and: [AstronautFilter] + or: [AstronautFilter] + not: AstronautFilter +} + +input AstronautOrder { + asc: AstronautOrderable + desc: AstronautOrderable + then: AstronautOrder +} + +input AstronautPatch { + missions: [MissionRef] +} + +input AstronautRef { + id: ID + missions: [MissionRef] +} + +input CarFilter { + id: [ID!] + has: CarHasFilter + and: [CarFilter] + or: [CarFilter] + not: CarFilter +} + +input CarOrder { + asc: CarOrderable + desc: CarOrderable + then: CarOrder +} + +input CarPatch { + name: String +} + +input CarRef { + id: ID + name: String +} + +input MissionFilter { + id: [ID!] + has: MissionHasFilter + and: [MissionFilter] + or: [MissionFilter] + not: MissionFilter +} + +input MissionOrder { + asc: MissionOrderable + desc: MissionOrderable + then: MissionOrder +} + +input MissionPatch { + crew: [AstronautRef] + designation: String + startDate: String + endDate: String +} + +input MissionRef { + id: ID + crew: [AstronautRef] + designation: String + startDate: String + endDate: String +} + +input UpdateAstronautInput { + filter: AstronautFilter! + set: AstronautPatch + remove: AstronautPatch +} + +input UpdateCarInput { + filter: CarFilter! + set: CarPatch + remove: CarPatch +} + +input UpdateMissionInput { + filter: MissionFilter! + set: MissionPatch + remove: MissionPatch +} + +####################### +# Generated Query +####################### + +type Query { + getMyFavoriteUsers(id: ID!): [User] + getMission(id: ID!): Mission + queryMission(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + aggregateMission(filter: MissionFilter): MissionAggregateResult + getCar(id: ID!): Car + queryCar(filter: CarFilter, order: CarOrder, first: Int, offset: Int): [Car] + aggregateCar(filter: CarFilter): CarAggregateResult +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addMission(input: [AddMissionInput!]!): AddMissionPayload + updateMission(input: UpdateMissionInput!): UpdateMissionPayload + deleteMission(filter: MissionFilter!): DeleteMissionPayload + addAstronaut(input: [AddAstronautInput!]!): AddAstronautPayload + updateAstronaut(input: UpdateAstronautInput!): UpdateAstronautPayload + deleteAstronaut(filter: AstronautFilter!): DeleteAstronautPayload + addCar(input: [AddCarInput!]!): AddCarPayload + updateCar(input: UpdateCarInput!): UpdateCarPayload + deleteCar(filter: CarFilter!): DeleteCarPayload +} + diff --git a/graphql/e2e/schema/schema_test.go b/graphql/e2e/schema/schema_test.go index 430ca79effc..b238eb63d88 100644 --- a/graphql/e2e/schema/schema_test.go +++ b/graphql/e2e/schema/schema_test.go @@ -595,6 +595,62 @@ func TestIntrospection(t *testing.T) { // introspection response or the JSON comparison. Needs deeper looking. } +func TestApolloServiceResolver(t *testing.T) { + schema := ` + type Mission { + id: ID! + crew: [Astronaut] + designation: String! + startDate: String + endDate: String + } + + type Astronaut @key(fields: "id") @extends { + id: ID! @external + missions: [Mission] + } + + type User @remote { + id: ID! + name: String! + } + + type Car @auth( + password: { rule: "{$ROLE: { eq: \"Admin\" } }"} + ){ + id: ID! + name: String! + } + + type Query { + getMyFavoriteUsers(id: ID!): [User] @custom(http: { + url: "http://my-api.com", + method: "GET" + }) + } + ` + common.SafelyUpdateGQLSchema(t, groupOneHTTP, schema, nil) + serviceQueryParams := &common.GraphQLParams{Query: ` + query { + _service { + s: sdl + } + }`} + resp := serviceQueryParams.ExecuteAsPost(t, groupOneGraphQLServer) + common.RequireNoGQLErrors(t, resp) + var gqlRes struct { + Service struct { + S string + } `json:"_service"` + } + require.NoError(t, json.Unmarshal(resp.Data, &gqlRes)) + + sdl, err := ioutil.ReadFile("apollo_service_response.graphql") + require.NoError(t, err) + + require.Equal(t, string(sdl), gqlRes.Service.S) +} + func TestDeleteSchemaAndExport(t *testing.T) { // first apply a schema schema := ` diff --git a/graphql/resolve/auth_query_test.yaml b/graphql/resolve/auth_query_test.yaml index 38e3f59c24f..391d53852f9 100644 --- a/graphql/resolve/auth_query_test.yaml +++ b/graphql/resolve/auth_query_test.yaml @@ -1835,3 +1835,108 @@ pwd as checkpwd(Post.pwd, "something") } } + +- name: "Entities query with query auth rules" + gqlquery: | + query { + _entities(representations: [{__typename: "Mission", id: "0x1"}{__typename: "Mission", id: "0x2"}, {__typename: "Mission", id: "0x3"}]) { + ... on Mission { + id + designation + startDate + } + } + } + jwtvar: + USER: "user" + dgquery: |- + query { + _entities(func: uid(_EntityRoot)) { + dgraph.type + id : Mission.id + designation : Mission.designation + startDate : Mission.startDate + dgraph.uid : uid + } + _EntityRoot as var(func: uid(Mission1)) @filter(uid(MissionAuth2)) + Mission1 as var(func: eq(Mission.id, "0x1", "0x2", "0x3")) @filter(type(Mission)) + MissionAuth2 as var(func: uid(Mission1)) @filter(eq(Mission.supervisorName, "user")) @cascade { + id : Mission.id + } + } +- name: "Entities query with top level RBAC rule true and level 1 query auth rule" + gqlquery: | + query { + _entities(representations: [{__typename: "Astronaut", id: "0x1"},{__typename: "Astronaut", id: "0x2"},{__typename: "Astronaut", id: "0x3"}]) { + ... on Astronaut { + missions { + designation + } + } + } + } + jwtvar: + ROLE: "admin" + USER: "user" + dgquery: |- + query { + _entities(func: uid(_EntityRoot)) { + dgraph.type + missions : Astronaut.missions @filter(uid(Mission1)) { + designation : Mission.designation + dgraph.uid : uid + } + dgraph.uid : uid + } + _EntityRoot as var(func: uid(Astronaut4)) + Astronaut4 as var(func: eq(Astronaut.id, "0x1", "0x2", "0x3")) @filter(type(Astronaut)) + var(func: uid(_EntityRoot)) { + Mission2 as Astronaut.missions + } + Mission1 as var(func: uid(Mission2)) @filter(uid(MissionAuth3)) + MissionAuth3 as var(func: uid(Mission2)) @filter(eq(Mission.supervisorName, "user")) @cascade { + id : Mission.id + } + } + +- name: "Entities query with RBAC rule false" + gqlquery: | + query { + _entities(representations: [{__typename: "Astronaut", id: "0x1"},{__typename: "Astronaut", id: "0x2"},{__typename: "Astronaut", id: "0x3"}]) { + ... on Astronaut { + missions { + designation + } + } + } + } + jwtvar: + ROLE: "user" + dgquery: |- + query { + _entities() + } + +- name: "Entities query with top RBAC rules true and missing JWT variable for level 1 query auth rule" + gqlquery: | + query { + _entities(representations: [{__typename: "Astronaut", id: "0x1"},{__typename: "Astronaut", id: "0x2"},{__typename: "Astronaut", id: "0x3"}]) { + ... on Astronaut { + missions { + designation + } + } + } + } + jwtvar: + ROLE: "admin" + dgquery: |- + query { + _entities(func: uid(_EntityRoot)) { + dgraph.type + dgraph.uid : uid + } + _EntityRoot as var(func: uid(Astronaut3)) + Astronaut3 as var(func: eq(Astronaut.id, "0x1", "0x2", "0x3")) @filter(type(Astronaut)) + } + diff --git a/graphql/resolve/auth_test.go b/graphql/resolve/auth_test.go index 3b56253b3cc..3e230b50cf5 100644 --- a/graphql/resolve/auth_test.go +++ b/graphql/resolve/auth_test.go @@ -254,7 +254,7 @@ func TestInvalidAuthInfo(t *testing.T) { require.NoError(t, err, "Unable to read schema file") authSchema, err := testutil.AppendJWKAndVerificationKey(sch) require.NoError(t, err) - _, err = schema.NewHandler(string(authSchema), false) + _, err = schema.NewHandler(string(authSchema), false, false) require.Error(t, err, fmt.Errorf("Expecting either JWKUrl or (VerificationKey, Algo), both were given")) } @@ -263,7 +263,7 @@ func TestMissingAudienceWithJWKUrl(t *testing.T) { require.NoError(t, err, "Unable to read schema file") authSchema, err := testutil.AppendAuthInfoWithJWKUrlAndWithoutAudience(sch) require.NoError(t, err) - _, err = schema.NewHandler(string(authSchema), false) + _, err = schema.NewHandler(string(authSchema), false, false) require.Error(t, err, fmt.Errorf("required field missing in Dgraph.Authorization: `Audience`")) } diff --git a/graphql/resolve/query_rewriter.go b/graphql/resolve/query_rewriter.go index a09d8608307..3bf7068f751 100644 --- a/graphql/resolve/query_rewriter.go +++ b/graphql/resolve/query_rewriter.go @@ -155,11 +155,134 @@ func (qr *queryRewriter) Rewrite( return passwordQuery(gqlQuery, authRw) case schema.AggregateQuery: return aggregateQuery(gqlQuery, authRw), nil + case schema.EntitiesQuery: + return entitiesQuery(gqlQuery, authRw) default: return nil, errors.Errorf("unimplemented query type %s", gqlQuery.QueryType()) } } +// entitiesQuery rewrites the Apollo `_entities` Query which is sent from the Apollo gateway to a DQL query. +// This query is sent to the Dgraph service to resolve types `extended` and defined by this service. +func entitiesQuery(field schema.Query, authRw *authRewriter) ([]*gql.GraphQuery, error) { + + // Input Argument to the Query is a List of "__typename" and "keyField" pair. + // For this type Extension:- + // extend type Product @key(fields: "upc") { + // upc: String @external + // reviews: [Review] + // } + // Input to the Query will be + // "_representations": [ + // { + // "__typename": "Product", + // "upc": "B00005N5PF" + // }, + // ... + // ] + + representations, ok := field.ArgValue("representations").([]interface{}) + if !ok { + return nil, fmt.Errorf("Error parsing `representations` argument") + } + typeNames := make(map[string]bool) + keyFieldValueList := make([]interface{}, 0) + keyFieldIsID := false + keyFieldName := "" + var err error + for i, rep := range representations { + representation, ok := rep.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("Error parsing in %dth item in the `_representations` argument", i) + } + + typename, ok := representation["__typename"].(string) + if !ok { + return nil, fmt.Errorf("Unable to extract __typename from %dth item in the `_representations` argument", i) + } + + // Store all the typeNames into an map to perfrom validation at last. + typeNames[typename] = true + keyFieldName, keyFieldIsID, err = field.KeyField(typename) + if err != nil { + return nil, err + } + keyFieldValue, ok := representation[keyFieldName] + if !ok { + return nil, fmt.Errorf("Unable to extract value for key field `%s` from %dth item in the `_representations` argument", keyFieldName, i) + } + keyFieldValueList = append(keyFieldValueList, keyFieldValue) + } + + // Return error if there was no typename extracted from the `_representations` argument. + if len(typeNames) == 0 { + return nil, fmt.Errorf("Expect one typename in `_representations` argument, got none") + } + + // Since we have restricted that all the typeNames for the inputs in the + // representation list should be same, we need to validate it and throw error + // if represenation of more than one type exists. + if len(typeNames) > 1 { + keys := make([]string, len(typeNames)) + i := 0 + for k := range typeNames { + keys[i] = k + i++ + } + return nil, fmt.Errorf("Expected only one unique typename in `_representations` argument, got: %v", keys) + } + + var typeName string + for k := range typeNames { + typeName = k + } + + typeDefn := field.BuildType(typeName) + rbac := authRw.evaluateStaticRules(typeDefn) + + dgQuery := &gql.GraphQuery{ + Attr: field.Name(), + } + + if rbac == schema.Negative { + dgQuery.Attr = dgQuery.Attr + "()" + return []*gql.GraphQuery{dgQuery}, nil + } + + // Construct Filter at Root Func. + // if keyFieldsIsID = true and keyFieldValueList = {"0x1", "0x2"} + // then query will be formed as:- + // _entities(func: uid("0x1", "0x2") { + // ... + // } + // if keyFieldsIsID = false then query will be like:- + // _entities(func: eq(keyFieldName,"0x1", "0x2") { + // ... + // } + + // If the key field is of ID type and is not an external field + // then we query it using the `uid` otherwise we treat it as string + // and query using `eq` function. + if keyFieldIsID && !typeDefn.Field(keyFieldName).IsExternal() { + addUIDFunc(dgQuery, convertIDs(keyFieldValueList)) + } else { + addEqFunc(dgQuery, typeDefn.DgraphPredicate(keyFieldName), keyFieldValueList) + } + // AddTypeFilter in as the Filter to the Root the Query. + // Query will be like :- + // _entities(func: ...) @filter(type(typeName)) { + // ... + // } + addTypeFilter(dgQuery, typeDefn) + + selectionAuth := addSelectionSetFrom(dgQuery, field, authRw) + addUID(dgQuery) + + dgQueries := authRw.addAuthQueries(typeDefn, []*gql.GraphQuery{dgQuery}, rbac) + return append(dgQueries, selectionAuth...), nil + +} + func aggregateQuery(query schema.Query, authRw *authRewriter) []*gql.GraphQuery { // Get the type which the count query is written for @@ -948,6 +1071,17 @@ func addUIDFunc(q *gql.GraphQuery, uids []uint64) { } } +func addEqFunc(q *gql.GraphQuery, dgPred string, values []interface{}) { + args := []gql.Arg{{Value: dgPred}} + for _, v := range values { + args = append(args, gql.Arg{Value: maybeQuoteArg("eq", v)}) + } + q.Func = &gql.Function{ + Name: "eq", + Args: args, + } +} + func addTypeFunc(q *gql.GraphQuery, typ string) { q.Func = buildTypeFunc(typ) } @@ -1254,7 +1388,9 @@ func addSelectionSetFrom( Alias: generateUniqueDgraphAlias(f, fieldSeenCount), } - if f.Type().Name() == schema.IDType { + // if field of IDType has @external directive then it means that + // it stored as String with Hash index internally in the dgraph. + if f.Type().Name() == schema.IDType && !f.IsExternal() { child.Attr = "uid" } else { child.Attr = f.DgraphPredicate() @@ -1690,14 +1826,33 @@ func buildFilter(typ schema.Type, filter map[string]interface{}) *gql.FilterTree }, }) case []interface{}: - // ids: [ 0x123, 0x124 ] -> uid(0x123, 0x124) - ids := convertIDs(dgFunc) - ands = append(ands, &gql.FilterTree{ - Func: &gql.Function{ - Name: "uid", - UID: ids, - }, - }) + // ids: [ 0x123, 0x124] + + // If ids is an @external field then it gets rewritten just like `in` filter + // ids: [0x123, 0x124] -> eq(typeName.ids, "0x123", 0x124) + if typ.Field(field).IsExternal() { + fn := "eq" + args := []gql.Arg{{Value: typ.DgraphPredicate(field)}} + for _, v := range dgFunc { + args = append(args, gql.Arg{Value: maybeQuoteArg(fn, v)}) + } + ands = append(ands, &gql.FilterTree{ + Func: &gql.Function{ + Name: fn, + Args: args, + }, + }) + } else { + // if it is not an @external field then it is rewritten as uid filter. + // ids: [ 0x123, 0x124 ] -> uid(0x123, 0x124) + ids := convertIDs(dgFunc) + ands = append(ands, &gql.FilterTree{ + Func: &gql.Function{ + Name: "uid", + UID: ids, + }, + }) + } case interface{}: // has: comments -> has(Post.comments) // OR diff --git a/graphql/resolve/query_test.yaml b/graphql/resolve/query_test.yaml index 3f80a8c39e2..ea5e19fa261 100644 --- a/graphql/resolve/query_test.yaml +++ b/graphql/resolve/query_test.yaml @@ -2474,7 +2474,6 @@ gqlquery: | query { getComment(id: "0x1") { - id author title content @@ -2487,10 +2486,10 @@ dgquery: |- query { getComment(func: uid(0x1)) @filter(type(Comment)) { - id : uid author : Comment.author title : Comment.title ups : Comment.ups + id : uid url : Comment.url } } @@ -3195,4 +3194,52 @@ name : Author.name dgraph.uid : uid } + } + +- + name: "entities query for extended type having @key field of ID type" + gqlquery: | + query { + _entities(representations: [{__typename: "Astronaut", id: "0x1" },{__typename: "Astronaut", id: "0x2" }]) { + ... on Astronaut { + missions { + designation + } + } + } + } + dgquery: |- + query { + _entities(func: eq(Astronaut.id, "0x1", "0x2")) @filter(type(Astronaut)) { + dgraph.type + missions : Astronaut.missions { + designation : Mission.designation + dgraph.uid : uid + } + dgraph.uid : uid + } + } + +- + name: "entities query for extended type having @key field of string type with @id directive" + gqlquery: | + query { + _entities(representations: [{__typename: "SpaceShip", id: "0x1" },{__typename: "SpaceShip", id: "0x2" }]) { + ... on SpaceShip { + missions { + designation + } + } + } + } + dgquery: |- + query { + _entities(func: eq(SpaceShip.id, "0x1", "0x2")) @filter(type(SpaceShip)) { + dgraph.type + missions : SpaceShip.missions { + designation : Mission.designation + dgraph.uid : uid + } + dgraph.uid : uid + } } \ No newline at end of file diff --git a/graphql/resolve/resolver.go b/graphql/resolve/resolver.go index 1774979a5f9..5eb653ec799 100644 --- a/graphql/resolve/resolver.go +++ b/graphql/resolve/resolver.go @@ -277,6 +277,7 @@ func (rf *resolverFactory) WithConventionResolvers( queries := append(s.Queries(schema.GetQuery), s.Queries(schema.FilterQuery)...) queries = append(queries, s.Queries(schema.PasswordQuery)...) queries = append(queries, s.Queries(schema.AggregateQuery)...) + queries = append(queries, s.Queries(schema.EntitiesQuery)...) for _, q := range queries { rf.WithQueryResolver(q, func(q schema.Query) QueryResolver { return NewQueryResolver(fns.Qrw, fns.Ex, StdQueryCompletion()) diff --git a/graphql/resolve/schema.graphql b/graphql/resolve/schema.graphql index 430902fab6a..8384af4bbc9 100644 --- a/graphql/resolve/schema.graphql +++ b/graphql/resolve/schema.graphql @@ -378,4 +378,25 @@ type Workflow { type Node { name: String! +} + +# test for entities resolver + +type Mission @key(fields: "id") { + id: String! @id + crew: [Astronaut] + spaceShip: [SpaceShip] + designation: String! + startDate: String + endDate: String +} + +type Astronaut @key(fields: "id") @extends { + id: ID! @external + missions: [Mission] +} + +type SpaceShip @key(fields: "id") @extends { + id: String! @id @external + missions: [Mission] } \ No newline at end of file diff --git a/graphql/schema/dgraph_schemagen_test.yml b/graphql/schema/dgraph_schemagen_test.yml index d4b5d6f14a8..3555b13d581 100644 --- a/graphql/schema/dgraph_schemagen_test.yml +++ b/graphql/schema/dgraph_schemagen_test.yml @@ -754,3 +754,35 @@ schemas: } T.id: float @index(float) @upsert . T.value: string . + - name: "type extension having @external field of ID type which is @key" + input: | + extend type Product @key(fields: "id") { + id: ID! @external + name: String! + price: Int @external + reviews: [String] + } + output: | + type Product { + Product.id + Product.name + Product.reviews + } + Product.id: string @index(hash) @upsert . + Product.name: string . + Product.reviews: [string] . + - name: "type extension having @external field of non ID type which is @key" + input: | + extend type Product @key(fields: "name") { + id: ID! @external + name: String! @id @external + reviews: [String] + } + output: | + type Product { + Product.name + Product.reviews + } + Product.name: string @index(hash) @upsert . + Product.reviews: [string] . + diff --git a/graphql/schema/gqlschema.go b/graphql/schema/gqlschema.go index 8f0274758d3..b881c281536 100644 --- a/graphql/schema/gqlschema.go +++ b/graphql/schema/gqlschema.go @@ -64,6 +64,12 @@ const ( cacheControlDirective = "cacheControl" CacheControlHeader = "Cache-Control" + // Directives to support Apollo Federation + apolloKeyDirective = "key" + apolloKeyArg = "fields" + apolloExternalDirective = "external" + apolloExtendsDirective = "extends" + // custom directive args and fields dqlArg = "dql" httpArg = "http" @@ -94,7 +100,9 @@ const ( // schemaExtras is everything that gets added to an input schema to make it // GraphQL valid and for the completion algorithm to use to build in search // capability into the schema. - schemaExtras = ` + + // Just remove directive definitions and not the input types + schemaInputs = ` """ The Int64 scalar type represents a signed 64‐bit numeric non‐fractional value. Int64 can represent values in range [-(2^63),(2^63 - 1)]. @@ -259,7 +267,8 @@ input GenerateMutationParams { update: Boolean delete: Boolean } - +` + directiveDefs = ` 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 @@ -281,7 +290,21 @@ directive @generate( query: GenerateQueryParams, mutation: GenerateMutationParams, subscription: Boolean) on OBJECT | INTERFACE +` + apolloSupportedDirectiveDefs = ` +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 @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM +directive @cascade(fields: [String]) on FIELD +directive @lambda on FIELD_DEFINITION +directive @cacheControl(maxAge: Int!) on QUERY +` + filterInputs = ` input IntFilter { eq: Int in: [Int] @@ -350,6 +373,25 @@ input StringHashFilter { eq: String in: [String] } +` + + apolloSchemaExtras = ` +scalar _Any +scalar _FieldSet + +type _Service { + sdl: String +} + +directive @external on FIELD_DEFINITION +directive @key(fields: _FieldSet!) on OBJECT | INTERFACE +directive @extends on OBJECT | INTERFACE +` + apolloSchemaQueries = ` +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} ` ) @@ -503,18 +545,21 @@ func ValidatorNoOp( } var directiveValidators = map[string]directiveValidator{ - inverseDirective: hasInverseValidation, - 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, - lambdaDirective: lambdaDirectiveValidation, - generateDirective: ValidatorNoOp, + inverseDirective: hasInverseValidation, + 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, + lambdaDirective: lambdaDirectiveValidation, + generateDirective: ValidatorNoOp, + apolloKeyDirective: ValidatorNoOp, + apolloExtendsDirective: ValidatorNoOp, + apolloExternalDirective: apolloExternalValidation, } // directiveLocationMap stores the directives and their locations for the ones which can be @@ -635,6 +680,7 @@ func copyAstFieldDef(src *ast.FieldDefinition) *ast.FieldDefinition { // expandSchema adds schemaExtras to the doc and adds any fields inherited from interfaces into // implementing types func expandSchema(doc *ast.SchemaDocument) *gqlerror.Error { + schemaExtras := schemaInputs + directiveDefs + filterInputs docExtras, gqlErr := parser.ParseSchema(&ast.Source{Input: schemaExtras}) if gqlErr != nil { x.Panic(gqlErr) @@ -710,9 +756,51 @@ func expandSchema(doc *ast.SchemaDocument) *gqlerror.Error { doc.Definitions = append(doc.Definitions, docExtras.Definitions...) doc.Directives = append(doc.Directives, docExtras.Directives...) + expandSchemaWithApolloExtras(doc) return nil } +func expandSchemaWithApolloExtras(doc *ast.SchemaDocument) { + var apolloKeyTypes []string + for _, defn := range doc.Definitions { + if defn.Directives.ForName(apolloKeyDirective) != nil { + apolloKeyTypes = append(apolloKeyTypes, defn.Name) + } + } + + // No need to Expand with Apollo federation Extras + if len(apolloKeyTypes) == 0 { + return + } + + // Form _Entity union with all the entities + // for e.g : union _Entity = A | B + // where A and B are object with @key directives + entityUnionDefinition := &ast.Definition{Kind: ast.Union, Name: "_Entity", Types: apolloKeyTypes} + doc.Definitions = append(doc.Definitions, entityUnionDefinition) + + // Parse Apollo Queries and append to the Parsed Schema + docApolloQueries, gqlErr := parser.ParseSchema(&ast.Source{Input: apolloSchemaQueries}) + if gqlErr != nil { + x.Panic(gqlErr) + } + + queryDefinition := doc.Definitions.ForName("Query") + if queryDefinition == nil { + doc.Definitions = append(doc.Definitions, docApolloQueries.Definitions[0]) + } else { + queryDefinition.Fields = append(queryDefinition.Fields, docApolloQueries.Definitions[0].Fields...) + } + + docExtras, gqlErr := parser.ParseSchema(&ast.Source{Input: apolloSchemaExtras}) + if gqlErr != nil { + x.Panic(gqlErr) + } + doc.Definitions = append(doc.Definitions, docExtras.Definitions...) + doc.Directives = append(doc.Directives, docExtras.Directives...) + +} + // preGQLValidation validates schema before GraphQL validation. Validation // before GraphQL validation means the schema only has allowed structures, and // means we can give better errors than GrqphQL validation would give if their @@ -811,7 +899,9 @@ func applyFieldValidations(typ *ast.Definition, field *ast.FieldDefinition) gqle // completeSchema generates all the required types and fields for // query/mutation/update for all the types mentioned in the schema. -func completeSchema(sch *ast.Schema, definitions []string) { +// In case of Apollo service Query, input types from queries and mutations +// are excluded due to the limited support currently. +func completeSchema(sch *ast.Schema, definitions []string, apolloServiceQuery bool) { query := sch.Types["Query"] if query != nil { query.Kind = ast.Object @@ -898,13 +988,17 @@ func completeSchema(sch *ast.Schema, definitions []string) { // types and inputs needed for query and search addFilterType(sch, defn) addTypeOrderable(sch, defn) - addFieldFilters(sch, defn) + addFieldFilters(sch, defn, apolloServiceQuery) addAggregationResultType(sch, defn) - addQueries(sch, defn, params) + // Don't expose queries for the @extends type to the gateway + // as it is resolved through `_entities` resolver. + if !(apolloServiceQuery && hasExtends(defn)) { + addQueries(sch, defn, params) + } addTypeHasFilter(sch, defn) // We need to call this at last as aggregateFields // should not be part of HasFilter or UpdatePayloadType etc. - addAggregateFields(sch, defn) + addAggregateFields(sch, defn, apolloServiceQuery) } } @@ -1048,8 +1142,16 @@ func addUnionMemberTypeEnum(schema *ast.Schema, defn *ast.Definition) { schema.Types[enumName] = enum } +// For extended Type definition, if Field with ID type is also field with @key directive then +// it should be present in the addTypeInput as it should not be generated automatically by dgraph +// but determined by the value of field in the GraphQL service where the type is defined. func addInputType(schema *ast.Schema, defn *ast.Definition) { field := getFieldsWithoutIDType(schema, defn) + if hasExtends(defn) { + idField := getIDField(defn) + field = append(idField, field...) + } + if len(field) != 0 { schema.Types["Add"+defn.Name+"Input"] = &ast.Definition{ Kind: ast.InputObject, @@ -1159,7 +1261,7 @@ func addPatchType(schema *ast.Schema, defn *ast.Definition) { // ... // } // } -func addFieldFilters(schema *ast.Schema, defn *ast.Definition) { +func addFieldFilters(schema *ast.Schema, defn *ast.Definition, apolloServiceQuery bool) { for _, fld := range defn.Fields { // Filtering and ordering for fields with @custom/@lambda directive is handled by the remote // endpoint. @@ -1167,6 +1269,11 @@ func addFieldFilters(schema *ast.Schema, defn *ast.Definition) { continue } + // Don't add Filters for @extended types as they can't be filtered. + if apolloServiceQuery && hasExtends(schema.Types[fld.Type.Name()]) { + continue + } + // Filtering makes sense both for lists (= return only items that match // this filter) and for singletons (= only have this value in the result // if it satisfies this filter) @@ -1190,8 +1297,14 @@ func addFieldFilters(schema *ast.Schema, defn *ast.Definition) { // The following aggregate field is added to type T // fieldAAggregate(filter : AFilter) : AAggregateResult // These fields are added to support aggregate queries like count, avg, min -func addAggregateFields(schema *ast.Schema, defn *ast.Definition) { +func addAggregateFields(schema *ast.Schema, defn *ast.Definition, apolloServiceQuery bool) { for _, fld := range defn.Fields { + + // Don't generate Aggregate Queries for field whose types are extended + // in the schema. + if apolloServiceQuery && hasExtends(schema.Types[fld.Type.Name()]) { + continue + } // Aggregate Fields only makes sense for fields of // list types of kind Object or Interface // (not scalar lists or not singleton types or lists of other kinds). @@ -1243,6 +1356,11 @@ func addTypeHasFilter(schema *ast.Schema, defn *ast.Definition) { if isID(fld) || hasCustomOrLambda(fld) { continue } + // Ignore Fields with @external directives also excluding those which are present + // as an argument in @key directive + if hasExternal(fld) && !isKeyField(fld, defn) { + continue + } filter.EnumValues = append(filter.EnumValues, &ast.EnumValueDefinition{Name: fld.Name}) } @@ -1362,6 +1480,12 @@ func addFilterType(schema *ast.Schema, defn *ast.Definition) { } for _, fld := range defn.Fields { + // Ignore Fields with @external directives also excluding those which are present + // as an argument in @key directive + if hasExternal(fld) && !isKeyField(fld, defn) { + continue + } + if isID(fld) { filter.Fields = append(filter.Fields, &ast.FieldDefinition{ @@ -1440,13 +1564,19 @@ func isEnumList(fld *ast.FieldDefinition, sch *ast.Schema) bool { } func hasOrderables(defn *ast.Definition) bool { - return fieldAny(defn.Fields, isOrderable) + return fieldAny(defn.Fields, func(fld *ast.FieldDefinition) bool { + return isOrderable(fld, defn) + }) } -func isOrderable(fld *ast.FieldDefinition) bool { +func isOrderable(fld *ast.FieldDefinition, defn *ast.Definition) bool { // lists can't be ordered and NamedType will be empty for lists, // so it will return false for list fields - return orderable[fld.Type.NamedType] && !hasCustomOrLambda(fld) + // External field can't be ordered except when it is a @key field + if !hasExternal(fld) { + return orderable[fld.Type.NamedType] && !hasCustomOrLambda(fld) + } + return isKeyField(fld, defn) } // Returns true if the field is of type which can be summed. Eg: int, int64, float @@ -1455,11 +1585,11 @@ func isSummable(fld *ast.FieldDefinition) bool { } func hasID(defn *ast.Definition) bool { - return fieldAny(defn.Fields, isID) + return fieldAny(nonExternalAndKeyFields(defn), isID) } func hasXID(defn *ast.Definition) bool { - return fieldAny(defn.Fields, hasIDDirective) + return fieldAny(nonExternalAndKeyFields(defn), hasIDDirective) } // fieldAny returns true if any field in fields satisfies pred @@ -1578,7 +1708,8 @@ func addTypeOrderable(schema *ast.Schema, defn *ast.Definition) { } for _, fld := range defn.Fields { - if isOrderable(fld) { + + if isOrderable(fld, defn) { order.EnumValues = append(order.EnumValues, &ast.EnumValueDefinition{Name: fld.Name}) } @@ -1695,7 +1826,7 @@ func addAggregationResultType(schema *ast.Schema, defn *ast.Definition) { } // Adds titleMax, titleMin fields for a field of name title. - if isOrderable(fld) { + if isOrderable(fld, defn) { minField := &ast.FieldDefinition{ Name: fld.Name + "Min", Type: aggregateFieldType, @@ -1739,7 +1870,6 @@ func addGetQuery(schema *ast.Schema, defn *ast.Definition, generateSubscription if !hasIDField && (defn.Kind == "INTERFACE" || !hasXIDField) { return } - qry := &ast.FieldDefinition{ Name: "get" + defn.Name, Type: &ast.Type{ @@ -1989,6 +2119,11 @@ func getNonIDFields(schema *ast.Schema, defn *ast.Definition) ast.FieldList { continue } + // Ignore Fields with @external directives also as they shouldn't be present + // in the Patch Type Also. + if hasExternal(fld) { + continue + } // Fields with @custom/@lambda directive should not be part of mutation input, // hence we skip them. if hasCustomOrLambda(fld) { @@ -2030,6 +2165,12 @@ func getFieldsWithoutIDType(schema *ast.Schema, defn *ast.Definition) ast.FieldL continue } + // Ignore Fields with @external directives and excluding those which are present + // as an argument in @key directive + if hasExternal(fld) && !isKeyField(fld, defn) { + continue + } + // Fields with @custom/@lambda directive should not be part of mutation input, // hence we skip them. if hasCustomOrLambda(fld) { @@ -2062,9 +2203,15 @@ func getIDField(defn *ast.Definition) ast.FieldList { fldList := make([]*ast.FieldDefinition, 0) for _, fld := range defn.Fields { if isIDField(defn, fld) { + // Excluding those fields which are external and are not @key. + if hasExternal(fld) && !isKeyField(fld, defn) { + continue + } newFld := *fld newFldType := *fld.Type newFld.Type = &newFldType + newFld.Directives = nil + newFld.Arguments = nil fldList = append(fldList, &newFld) break } @@ -2088,9 +2235,15 @@ func getXIDField(defn *ast.Definition) ast.FieldList { fldList := make([]*ast.FieldDefinition, 0) for _, fld := range defn.Fields { if hasIDDirective(fld) { + // Excluding those fields which are external and are not @key. + if hasExternal(fld) && !isKeyField(fld, defn) { + continue + } newFld := *fld newFldType := *fld.Type newFld.Type = &newFldType + newFld.Directives = nil + newFld.Arguments = nil fldList = append(fldList, &newFld) break } @@ -2244,7 +2397,10 @@ func generateUnionString(typ *ast.Definition) string { // Any types in originalTypes are printed first, followed by the schemaExtras, // and then all generated types, scalars, enums, directives, query and // mutations all in alphabetical order. -func Stringify(schema *ast.Schema, originalTypes []string) string { +// var "apolloServiceQuery" is used to distinguish Schema String from what should be +// returned as a result of apollo service query. In case of Apollo service query, Schema +// removes some of the directive definitions which are currently not supported at the gateway. +func Stringify(schema *ast.Schema, originalTypes []string, apolloServiceQuery bool) string { var sch, original, object, input, enum strings.Builder if schema.Types == nil { @@ -2252,7 +2408,9 @@ func Stringify(schema *ast.Schema, originalTypes []string) string { } printed := make(map[string]bool) - + // Marked "_Service" type as printed as it will be printed in the + // Extended Apollo Definitions + printed["_Service"] = true // 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 { @@ -2279,6 +2437,12 @@ func Stringify(schema *ast.Schema, originalTypes []string) string { // schemaExtras gets added to the result as a string, but we need to mark // off all it's contents as printed, so nothing in there gets printed with // the generated definitions. + // In case of ApolloServiceQuery, schemaExtras is little different. + // It excludes some of the directive definitions. + schemaExtras := schemaInputs + directiveDefs + filterInputs + if apolloServiceQuery { + schemaExtras = schemaInputs + apolloSupportedDirectiveDefs + filterInputs + } docExtras, gqlErr := parser.ParseSchema(&ast.Source{Input: schemaExtras}) if gqlErr != nil { x.Panic(gqlErr) @@ -2328,6 +2492,14 @@ func Stringify(schema *ast.Schema, originalTypes []string) string { "#######################\n# Extended Definitions\n#######################\n")) x.Check2(sch.WriteString(schemaExtras)) x.Check2(sch.WriteString("\n")) + // Add Apollo Extras to the schema only when "_Entity" union is generated. + if schema.Types["_Entity"] != nil { + x.Check2(sch.WriteString( + "#######################\n# Extended Apollo Definitions\n#######################\n")) + x.Check2(sch.WriteString(generateUnionString(schema.Types["_Entity"]))) + x.Check2(sch.WriteString(apolloSchemaExtras)) + x.Check2(sch.WriteString("\n")) + } if object.Len() > 0 { x.Check2(sch.WriteString( "#######################\n# Generated Types\n#######################\n\n")) @@ -2374,7 +2546,7 @@ func idTypeFor(defn *ast.Definition) string { } func xidTypeFor(defn *ast.Definition) (string, string) { - for _, fld := range defn.Fields { + for _, fld := range nonExternalAndKeyFields(defn) { if hasIDDirective(fld) { return fld.Name, fld.Type.Name() } diff --git a/graphql/schema/gqlschema_test.yml b/graphql/schema/gqlschema_test.yml index bec7df72d2c..5dac8a5c17f 100644 --- a/graphql/schema/gqlschema_test.yml +++ b/graphql/schema/gqlschema_test.yml @@ -2673,6 +2673,93 @@ invalid_schemas: errlist: [ { "message": "Field I3.name can only be defined once.", "locations": [ { "line": 2, "column": 5 } ] }, ] + - name: "@external directive can only be used on fields of Type Extension" + input: | + type Product @key(fields: "id") { + id: ID! @external + reviews: String + } + errlist: [ + { "message": "Type Product: Field id: @external directive can only be defined on fields in type extensions. i.e., the type must have `@extends` or use `extend` keyword.", "locations": [ { "line": 2, "column": 14 } ] }, + ] + - name: "@key directive defined more than once" + input: | + type Product @key(fields: "id") @key(fields: "name") { + id: ID! + name: String! @id + reviews: String + } + errlist: [ + { "message": "Type Product; @key directive should not be defined more than once.", "locations": [ { "line": 1, "column": 34 } ] }, + ] + - name: "Argument inside @key directive uses field not defined in the type" + input: | + type Product @key(fields: "username") { + id: ID! + name: String! @id + reviews: String + } + errlist: [ + { "message": "Type Product; @key directive uses a field username which is not defined inside the type.", "locations": [ { "line": 1, "column":19 } ] }, + ] + - name: "Argument inside @key directive must have ID field or field with @id directive" + input: | + extend type Product @key(fields: "name") { + id: ID! @external + name: String! @external + reviews: String + } + errlist: [ + { "message": "Type Product: Field name: used inside @key directive should be of type ID or have @id directive.", "locations": [ { "line": 1, "column": 26 } ] }, + ] + - name: "@extends directive without @key directive" + input: | + type Product @extends{ + id: ID! @external + name: String! @external + reviews: [Reviews] + } + + type Reviews @key(fields: "id") { + id: ID! + review: String! + } + errlist: [ + {"message": "Type Product; Type Extension cannot be defined without @key directive", "locations": [ { "line": 11, "column": 12} ] }, + ] + - name: "@remote directive with @key" + input: | + type Product @remote @key(fields: "id"){ + id: ID! + name: String! + reviews: [Reviews] + } + + type Reviews @key(fields: "id") { + id: ID! + review: String! + } + errlist: [ + {"message": "Type Product; @remote directive cannot be defined with @key directive", "locations": [ { "line": 174, "column": 12} ] }, + ] + - name: "directives defined on @external fields that are not @key." + input: | + extend type Product @key(fields: "id"){ + id: ID! @external + name: String! @search @external + reviews: [Reviews] + } + + type Reviews @key(fields: "id") { + id: ID! + review: String! + } + errlist: [ + {"message": "Type Product: Field name: @search directive can not be defined on @external fields that are not @key.", "locations": [ { "line": 3, "column": 18} ] }, + ] + + + valid_schemas: diff --git a/graphql/schema/rules.go b/graphql/schema/rules.go index d20ce1c32f7..ddd1197552d 100644 --- a/graphql/schema/rules.go +++ b/graphql/schema/rules.go @@ -39,7 +39,7 @@ func init() { schemaValidations = append(schemaValidations, dgraphDirectivePredicateValidation) typeValidations = append(typeValidations, idCountCheck, dgraphDirectiveTypeValidation, passwordDirectiveValidation, conflictingDirectiveValidation, nonIdFieldsCheck, - remoteTypeValidation, generateDirectiveValidation) + remoteTypeValidation, generateDirectiveValidation, apolloKeyValidation, apolloExtendsValidation) fieldValidations = append(fieldValidations, listValidityCheck, fieldArgumentCheck, fieldNameCheck, isValidFieldForList, hasAuthDirective) @@ -2003,6 +2003,100 @@ func idValidation(sch *ast.Schema, typ.Name, field.Name, field.Type.String())} } +func apolloKeyValidation(sch *ast.Schema, typ *ast.Definition) gqlerror.List { + dirList := typ.Directives.ForNames(apolloKeyDirective) + if len(dirList) == 0 { + return nil + } + + if len(dirList) > 1 { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dirList[1].Position, + "Type %s; @key directive should not be defined more than once.", typ.Name)} + } + dir := dirList[0] + arg := dir.Arguments.ForName(apolloKeyArg) + if arg == nil || arg.Value.Raw == "" { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s; Argument %s inside @key directive must be defined.", typ.Name, apolloKeyArg)} + } + + fld := typ.Fields.ForName(arg.Value.Raw) + if fld == nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + arg.Position, + "Type %s; @key directive uses a field %s which is not defined inside the type.", typ.Name, arg.Value.Raw)} + } + + if !(isID(fld) || hasIDDirective(fld)) { + return []*gqlerror.Error{gqlerror.ErrorPosf( + arg.Position, + "Type %s: Field %s: used inside @key directive should be of type ID or have @id directive.", typ.Name, fld.Name)} + } + + remoteDirective := typ.Directives.ForName(remoteDirective) + if remoteDirective != nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + remoteDirective.Definition.Position, + "Type %s; @remote directive cannot be defined with @key directive", typ.Name)} + } + return nil +} + +func apolloExtendsValidation(sch *ast.Schema, typ *ast.Definition) gqlerror.List { + extendsDirective := typ.Directives.ForName(apolloExtendsDirective) + if extendsDirective == nil { + return nil + } + keyDirective := typ.Directives.ForName(apolloKeyDirective) + if keyDirective == nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + extendsDirective.Definition.Position, + "Type %s; Type Extension cannot be defined without @key directive", typ.Name)} + } + remoteDirective := typ.Directives.ForName(remoteDirective) + if remoteDirective != nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + remoteDirective.Definition.Position, + "Type %s; @remote directive cannot be defined with @extends directive", typ.Name)} + } + return nil +} + +func apolloExternalValidation(sch *ast.Schema, + typ *ast.Definition, + field *ast.FieldDefinition, + dir *ast.Directive, + secrets map[string]x.SensitiveByteSlice) gqlerror.List { + + extendsDirective := typ.Directives.ForName(apolloExtendsDirective) + if extendsDirective == nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s: Field %s: @external directive can only be defined on fields in type extensions. i.e., the type must have `@extends` or use `extend` keyword.", typ.Name, field.Name)} + } + + if hasCustomOrLambda(field) { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dir.Position, + "Type %s: Field %s: @external directive can not be defined on fields with @custom or @lambda directive.", typ.Name, field.Name)} + } + + if !isKeyField(field, typ) { + directiveList := []string{inverseDirective, searchDirective, dgraphDirective, idDirective} + for _, directive := range directiveList { + dirDefn := field.Directives.ForName(directive) + if dirDefn != nil { + return []*gqlerror.Error{gqlerror.ErrorPosf( + dirDefn.Position, + "Type %s: Field %s: @%s directive can not be defined on @external fields that are not @key.", typ.Name, field.Name, directive)} + } + } + } + return nil +} + func searchMessage(sch *ast.Schema, field *ast.FieldDefinition) string { var possibleSearchArgs []string for name, typ := range supportedSearches { diff --git a/graphql/schema/schemagen.go b/graphql/schema/schemagen.go index d09ef16557a..319ee947e25 100644 --- a/graphql/schema/schemagen.go +++ b/graphql/schema/schemagen.go @@ -37,6 +37,7 @@ import ( type Handler interface { DGSchema() string GQLSchema() string + GQLSchemaWithoutApolloExtras() string } type handler struct { @@ -65,13 +66,108 @@ func FromString(schema string) (Schema, error) { } func (s *handler) GQLSchema() string { - return Stringify(s.completeSchema, s.originalDefs) + return Stringify(s.completeSchema, s.originalDefs, false) } func (s *handler) DGSchema() string { return s.dgraphSchema } +// GQLSchemaWithoutApolloExtras return GraphQL schema string +// excluding Apollo extras definitions and Apollo Queries and +// some directives which are not exposed to the Apollo Gateway +// as they are failing in the schema validation which is a bug +// in their library. See here: +// https://github.com/apollographql/apollo-server/issues/3655 +func (s *handler) GQLSchemaWithoutApolloExtras() string { + typeMapCopy := make(map[string]*ast.Definition) + for typ, defn := range s.completeSchema.Types { + // Exclude "union _Entity = ..." definition from types + if typ == "_Entity" { + continue + } + fldListCopy := make(ast.FieldList, 0) + for _, fld := range defn.Fields { + fldDirectiveListCopy := make(ast.DirectiveList, 0) + for _, dir := range fld.Directives { + // Drop "@custom" directive from the field's definition. + if dir.Name == "custom" { + continue + } + fldDirectiveListCopy = append(fldDirectiveListCopy, dir) + } + newFld := &ast.FieldDefinition{ + Name: fld.Name, + Arguments: fld.Arguments, + DefaultValue: fld.DefaultValue, + Type: fld.Type, + Directives: fldDirectiveListCopy, + Position: fld.Position, + } + fldListCopy = append(fldListCopy, newFld) + } + + directiveListCopy := make(ast.DirectiveList, 0) + for _, dir := range defn.Directives { + // Drop @generate and @auth directive from the Type Definition. + if dir.Name == "generate" || dir.Name == "auth" { + continue + } + directiveListCopy = append(directiveListCopy, dir) + } + typeMapCopy[typ] = &ast.Definition{ + Kind: defn.Kind, + Name: defn.Name, + Directives: directiveListCopy, + Fields: fldListCopy, + BuiltIn: defn.BuiltIn, + EnumValues: defn.EnumValues, + } + } + queryList := make(ast.FieldList, 0) + for _, qry := range s.completeSchema.Query.Fields { + // Drop Apollo Queries from the List of Queries. + if qry.Name == "_entities" || qry.Name == "_service" { + continue + } + qryDirectiveListCopy := make(ast.DirectiveList, 0) + for _, dir := range qry.Directives { + // Drop @custom directive from the Queries. + if dir.Name == "custom" { + continue + } + qryDirectiveListCopy = append(qryDirectiveListCopy, dir) + } + queryList = append(queryList, &ast.FieldDefinition{ + Name: qry.Name, + Arguments: qry.Arguments, + Type: qry.Type, + Directives: qryDirectiveListCopy, + Position: qry.Position, + }) + } + + if typeMapCopy["Query"] != nil { + typeMapCopy["Query"].Fields = queryList + } + + queryDefn := &ast.Definition{ + Kind: ast.Object, + Name: "Query", + Fields: queryList, + } + astSchemaCopy := &ast.Schema{ + Query: queryDefn, + Mutation: s.completeSchema.Mutation, + Subscription: s.completeSchema.Subscription, + Types: typeMapCopy, + Directives: s.completeSchema.Directives, + PossibleTypes: s.completeSchema.PossibleTypes, + Implements: s.completeSchema.Implements, + } + return Stringify(astSchemaCopy, s.originalDefs, true) +} + func parseSecrets(sch string) (map[string]string, *authorization.AuthMeta, error) { m := make(map[string]string) scanner := bufio.NewScanner(strings.NewReader(sch)) @@ -125,7 +221,7 @@ func parseSecrets(sch string) (map[string]string, *authorization.AuthMeta, error // NewHandler processes the input schema. If there are no errors, it returns // a valid Handler, otherwise it returns nil and an error. -func NewHandler(input string, validateOnly bool) (Handler, error) { +func NewHandler(input string, validateOnly bool, apolloServiceQuery bool) (Handler, error) { if input == "" { return nil, gqlerror.Errorf("No schema specified") } @@ -173,6 +269,14 @@ func NewHandler(input string, validateOnly bool) (Handler, error) { return nil, gqlerror.List{gqlErr} } + // Convert All the Type Extensions into the Type Definitions with @external directive + // to maintain uniformity in the output schema + for _, ext := range doc.Extensions { + ext.Directives = append(ext.Directives, &ast.Directive{Name: "extends"}) + } + doc.Definitions = append(doc.Definitions, doc.Extensions...) + doc.Extensions = nil + gqlErrList := preGQLValidation(doc) if gqlErrList != nil { return nil, gqlErrList @@ -215,7 +319,7 @@ func NewHandler(input string, validateOnly bool) (Handler, error) { headers := getAllowedHeaders(sch, defns, authHeader) dgSchema := genDgSchema(sch, typesToComplete) - completeSchema(sch, typesToComplete) + completeSchema(sch, typesToComplete, apolloServiceQuery) cleanSchema(sch) if len(sch.Query.Fields) == 0 && len(sch.Mutation.Fields) == 0 { @@ -427,7 +531,18 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string) string { pwdField := getPasswordField(def) for _, f := range def.Fields { - if f.Type.Name() == "ID" || hasCustomOrLambda(f) { + if hasCustomOrLambda(f) { + continue + } + + // Ignore @external fields which are not @key + if hasExternal(f) && !isKeyField(f, def) { + continue + } + + // If a field of type ID has @external directive and is a @key field then + // it should be translated into a dgraph field with string type having hash index. + if f.Type.Name() == "ID" && !(hasExternal(f) && isKeyField(f, def)) { continue } @@ -477,23 +592,29 @@ func genDgSchema(gqlSch *ast.Schema, definitions []string) string { } typ.fields = append(typ.fields, field{fname, parentInt != nil}) case ast.Scalar: + fldType := inbuiltTypeToDgraph[f.Type.Name()] + // fldType can be "uid" only in case if it is @external and @key + // in this case it needs to be stored as string in dgraph. + if fldType == "uid" { + fldType = "string" + } typStr = fmt.Sprintf( "%s%s%s", - prefix, inbuiltTypeToDgraph[f.Type.Name()], suffix, + prefix, fldType, suffix, ) var indexes []string upsertStr := "" search := f.Directives.ForName(searchDirective) id := f.Directives.ForName(idDirective) - if id != nil { + if id != nil || f.Type.Name() == "ID" { upsertStr = "@upsert " switch f.Type.Name() { case "Int", "Int64": indexes = append(indexes, "int") case "Float": indexes = append(indexes, "float") - case "String": + case "String", "ID": indexes = append(indexes, "hash") } } diff --git a/graphql/schema/schemagen_test.go b/graphql/schema/schemagen_test.go index 18b4e7b763e..6558b3402d4 100644 --- a/graphql/schema/schemagen_test.go +++ b/graphql/schema/schemagen_test.go @@ -55,7 +55,7 @@ func TestDGSchemaGen(t *testing.T) { for _, sch := range schemas { t.Run(sch.Name, func(t *testing.T) { - schHandler, errs := NewHandler(sch.Input, false) + schHandler, errs := NewHandler(sch.Input, false, false) require.NoError(t, errs) dgSchema := schHandler.DGSchema() @@ -84,7 +84,7 @@ func TestSchemaString(t *testing.T) { str1, err := ioutil.ReadFile(inputFileName) require.NoError(t, err) - schHandler, errs := NewHandler(string(str1), false) + schHandler, errs := NewHandler(string(str1), false, false) require.NoError(t, errs) newSchemaStr := schHandler.GQLSchema() @@ -102,6 +102,36 @@ func TestSchemaString(t *testing.T) { } } +func TestApolloServiceQueryResult(t *testing.T) { + inputDir := "testdata/apolloservice/input/" + outputDir := "testdata/apolloservice/output/" + + files, err := ioutil.ReadDir(inputDir) + require.NoError(t, err) + + for _, testFile := range files { + t.Run(testFile.Name(), func(t *testing.T) { + inputFileName := inputDir + testFile.Name() + str1, err := ioutil.ReadFile(inputFileName) + require.NoError(t, err) + + schHandler, errs := NewHandler(string(str1), false, true) + require.NoError(t, errs) + + apolloServiceResult := schHandler.GQLSchemaWithoutApolloExtras() + + _, err = FromString(schHandler.GQLSchema()) + require.NoError(t, err) + outputFileName := outputDir + testFile.Name() + str2, err := ioutil.ReadFile(outputFileName) + require.NoError(t, err) + if diff := cmp.Diff(string(str2), apolloServiceResult); diff != "" { + t.Errorf("result mismatch - diff (- want +got):\n%s", diff) + } + }) + } +} + func TestSchemas(t *testing.T) { fileName := "gqlschema_test.yml" byts, err := ioutil.ReadFile(fileName) @@ -114,7 +144,7 @@ func TestSchemas(t *testing.T) { t.Run("Valid Schemas", func(t *testing.T) { for _, sch := range tests["valid_schemas"] { t.Run(sch.Name, func(t *testing.T) { - schHandler, errlist := NewHandler(sch.Input, false) + schHandler, errlist := NewHandler(sch.Input, false, false) require.NoError(t, errlist, sch.Name) newSchemaStr := schHandler.GQLSchema() @@ -128,7 +158,7 @@ func TestSchemas(t *testing.T) { t.Run("Invalid Schemas", func(t *testing.T) { for _, sch := range tests["invalid_schemas"] { t.Run(sch.Name, func(t *testing.T) { - _, errlist := NewHandler(sch.Input, false) + _, errlist := NewHandler(sch.Input, false, false) if diff := cmp.Diff(sch.Errlist, errlist, cmpopts.IgnoreUnexported(gqlerror.Error{})); diff != "" { t.Errorf("error mismatch (-want +got):\n%s", diff) } @@ -154,7 +184,7 @@ func TestAuthSchemas(t *testing.T) { t.Run("Valid Schemas", func(t *testing.T) { for _, sch := range tests["valid_schemas"] { t.Run(sch.Name, func(t *testing.T) { - schHandler, errlist := NewHandler(sch.Input, false) + schHandler, errlist := NewHandler(sch.Input, false, false) require.NoError(t, errlist, sch.Name) _, authError := FromString(schHandler.GQLSchema()) @@ -166,7 +196,7 @@ func TestAuthSchemas(t *testing.T) { t.Run("Invalid Schemas", func(t *testing.T) { for _, sch := range tests["invalid_schemas"] { t.Run(sch.Name, func(t *testing.T) { - schHandler, errlist := NewHandler(sch.Input, false) + schHandler, errlist := NewHandler(sch.Input, false, false) require.NoError(t, errlist, sch.Name) _, authError := FromString(schHandler.GQLSchema()) @@ -305,7 +335,7 @@ func TestOnlyCorrectSearchArgsWork(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - _, errlist := NewHandler(test.schema, false) + _, errlist := NewHandler(test.schema, false, false) require.Len(t, errlist, test.expectedErrors, "every field in this test applies @search wrongly and should raise an error") }) diff --git a/graphql/schema/testdata/apolloservice/input/auth-directive.graphql b/graphql/schema/testdata/apolloservice/input/auth-directive.graphql new file mode 100644 index 00000000000..0684e15dc66 --- /dev/null +++ b/graphql/schema/testdata/apolloservice/input/auth-directive.graphql @@ -0,0 +1,66 @@ +type Todo @secret(field: "pwd") @auth( + password: { rule: "{$ROLE: { eq: \"Admin\" } }"}, + query: { + or: [ + { rule: """ + query($X_MyApp_User: String!) { + queryTodo { + owner (filter: { username: { eq: $X_MyApp_User }}) { + username + } + } + }""" }, + { rule: """ + query($X_MyApp_User: String!) { + queryTodo { + sharedWith (filter: { username: { eq: $X_MyApp_User }}) { + username + } + } + }""" }, + { rule: """ + query { + queryTodo(filter: { isPublic: true }) { + id + } + }""" }, + ] + }, + add: { rule: """ + query($X_MyApp_User: String!) { + queryTodo { + owner (filter: { username: { eq: $X_MyApp_User }}) { + username + } + } + }""" }, + update: { rule: """ + query($X_MyApp_User: String!) { + queryTodo { + owner (filter: { username: { eq: $X_MyApp_User }}) { + username + } + } + }""" }, +) { + id: ID! + title: String + text: String + isPublic: Boolean @search + dateCompleted: String @search + sharedWith: [User] + owner: User @hasInverse(field: "todos") + somethingPrivate: String +} + +type User @key(fields: "username") @auth( + update: { rule: """ + query($X_MyApp_User: String!) { + queryUser(filter: { username: { eq: $X_MyApp_User }}) { + username + } + }""" } +){ + username: String! @id + todos: [Todo] +} diff --git a/graphql/schema/testdata/apolloservice/input/custom-directive.graphql b/graphql/schema/testdata/apolloservice/input/custom-directive.graphql new file mode 100644 index 00000000000..98d045fd88e --- /dev/null +++ b/graphql/schema/testdata/apolloservice/input/custom-directive.graphql @@ -0,0 +1,16 @@ +type User @remote { + id: ID! + name: String! +} + +type Car @key(fields: "id"){ + id: ID! + name: String! +} + +type Query { + getMyFavoriteUsers(id: ID!): [User] @custom(http: { + url: "http://my-api.com", + method: "GET" + }) +} \ No newline at end of file diff --git a/graphql/schema/testdata/apolloservice/input/extended-types.graphql b/graphql/schema/testdata/apolloservice/input/extended-types.graphql new file mode 100644 index 00000000000..fae8a433553 --- /dev/null +++ b/graphql/schema/testdata/apolloservice/input/extended-types.graphql @@ -0,0 +1,12 @@ +type Mission @key(fields: "id") { + id: ID! + crew: [Astronaut] + designation: String! + startDate: String + endDate: String +} + +type Astronaut @key(fields: "id") @extends { + id: ID! @external + missions: [Mission] +} \ No newline at end of file diff --git a/graphql/schema/testdata/apolloservice/input/generate-directive.graphql b/graphql/schema/testdata/apolloservice/input/generate-directive.graphql new file mode 100644 index 00000000000..0621754ede9 --- /dev/null +++ b/graphql/schema/testdata/apolloservice/input/generate-directive.graphql @@ -0,0 +1,37 @@ +interface Character @secret(field: "password") @generate( + query: { + get: false, + password: false + }, + subscription: false +) { + id: ID! + name: String! @search(by: [exact]) + friends: [Character] +} + +type Human implements Character @generate( + query: { + aggregate: true + }, + subscription: true +) { + totalCredits: Int +} + +type Person @withSubscription @generate( + query: { + get: false, + query: true, + password: true, + aggregate: false + }, + mutation: { + add: false, + delete: false + }, + subscription: false +) { + id: ID! + name: String! +} \ No newline at end of file diff --git a/graphql/schema/testdata/apolloservice/output/auth-directive.graphql b/graphql/schema/testdata/apolloservice/output/auth-directive.graphql new file mode 100644 index 00000000000..aeb264de8d2 --- /dev/null +++ b/graphql/schema/testdata/apolloservice/output/auth-directive.graphql @@ -0,0 +1,476 @@ +####################### +# Input Schema +####################### + +type Todo @secret(field: "pwd") { + id: ID! + title: String + text: String + isPublic: Boolean @search + dateCompleted: String @search + sharedWith(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + owner(filter: UserFilter): User @hasInverse(field: "todos") + somethingPrivate: String + sharedWithAggregate(filter: UserFilter): UserAggregateResult +} + +type User @key(fields: "username") { + username: String! @id + todos(filter: TodoFilter, order: TodoOrder, first: Int, offset: Int): [Todo] @hasInverse(field: owner) + todosAggregate(filter: TodoFilter): TodoAggregateResult +} + +####################### +# 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 + +input IntRange{ + min: Int! + max: Int! +} + +input FloatRange{ + min: Float! + max: Float! +} + +input Int64Range{ + min: Int64! + max: Int64! +} + +input DateTimeRange{ + min: DateTime! + max: DateTime! +} + +input StringRange{ + min: String! + max: String! +} + +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 + within: WithinFilter +} + +type PointList { + points: [Point!]! +} + +input PointListRef { + points: [PointRef!]! +} + +type Polygon { + coordinates: [PointList!]! +} + +input PolygonRef { + coordinates: [PointListRef!]! +} + +type MultiPolygon { + polygons: [Polygon!]! +} + +input MultiPolygonRef { + polygons: [PolygonRef!]! +} + +input WithinFilter { + polygon: PolygonRef! +} + +input ContainsFilter { + point: PointRef + polygon: PolygonRef +} + +input IntersectsFilter { + polygon: PolygonRef + multiPolygon: MultiPolygonRef +} + +input PolygonGeoFilter { + near: NearFilter + within: WithinFilter + contains: ContainsFilter + intersects: IntersectsFilter +} + +input GenerateQueryParams { + get: Boolean + query: Boolean + password: Boolean + aggregate: Boolean +} + +input GenerateMutationParams { + add: Boolean + update: Boolean + delete: Boolean +} + +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 @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM +directive @cascade(fields: [String]) on FIELD +directive @lambda on FIELD_DEFINITION +directive @cacheControl(maxAge: Int!) on QUERY + +input IntFilter { + eq: Int + in: [Int] + le: Int + lt: Int + ge: Int + gt: Int + between: IntRange +} + +input Int64Filter { + eq: Int64 + in: [Int64] + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 + between: Int64Range +} + +input FloatFilter { + eq: Float + in: [Float] + le: Float + lt: Float + ge: Float + gt: Float + between: FloatRange +} + +input DateTimeFilter { + eq: DateTime + in: [DateTime] + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime + between: DateTimeRange +} + +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 + between: StringRange +} + +input StringHashFilter { + eq: String + in: [String] +} + +####################### +# Generated Types +####################### + +type AddTodoPayload { + todo(filter: TodoFilter, order: TodoOrder, first: Int, offset: Int): [Todo] + numUids: Int +} + +type AddUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + numUids: Int +} + +type DeleteTodoPayload { + todo(filter: TodoFilter, order: TodoOrder, first: Int, offset: Int): [Todo] + msg: String + numUids: Int +} + +type DeleteUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + msg: String + numUids: Int +} + +type TodoAggregateResult { + count: Int + titleMin: String + titleMax: String + textMin: String + textMax: String + dateCompletedMin: String + dateCompletedMax: String + somethingPrivateMin: String + somethingPrivateMax: String +} + +type UpdateTodoPayload { + todo(filter: TodoFilter, order: TodoOrder, first: Int, offset: Int): [Todo] + numUids: Int +} + +type UpdateUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + numUids: Int +} + +type UserAggregateResult { + count: Int + usernameMin: String + usernameMax: String +} + +####################### +# Generated Enums +####################### + +enum TodoHasFilter { + title + text + isPublic + dateCompleted + sharedWith + owner + somethingPrivate +} + +enum TodoOrderable { + title + text + dateCompleted + somethingPrivate +} + +enum UserHasFilter { + username + todos +} + +enum UserOrderable { + username +} + +####################### +# Generated Inputs +####################### + +input AddTodoInput { + title: String + text: String + isPublic: Boolean + dateCompleted: String + sharedWith: [UserRef] + owner: UserRef + somethingPrivate: String + pwd: String! +} + +input AddUserInput { + username: String! + todos: [TodoRef] +} + +input TodoFilter { + id: [ID!] + isPublic: Boolean + dateCompleted: StringTermFilter + has: TodoHasFilter + and: [TodoFilter] + or: [TodoFilter] + not: TodoFilter +} + +input TodoOrder { + asc: TodoOrderable + desc: TodoOrderable + then: TodoOrder +} + +input TodoPatch { + title: String + text: String + isPublic: Boolean + dateCompleted: String + sharedWith: [UserRef] + owner: UserRef + somethingPrivate: String + pwd: String +} + +input TodoRef { + id: ID + title: String + text: String + isPublic: Boolean + dateCompleted: String + sharedWith: [UserRef] + owner: UserRef + somethingPrivate: String + pwd: String +} + +input UpdateTodoInput { + filter: TodoFilter! + set: TodoPatch + remove: TodoPatch +} + +input UpdateUserInput { + filter: UserFilter! + set: UserPatch + remove: UserPatch +} + +input UserFilter { + username: StringHashFilter + has: UserHasFilter + and: [UserFilter] + or: [UserFilter] + not: UserFilter +} + +input UserOrder { + asc: UserOrderable + desc: UserOrderable + then: UserOrder +} + +input UserPatch { + todos: [TodoRef] +} + +input UserRef { + username: String + todos: [TodoRef] +} + +####################### +# Generated Query +####################### + +type Query { + getTodo(id: ID!): Todo + checkTodoPassword(id: ID!, pwd: String!): Todo + queryTodo(filter: TodoFilter, order: TodoOrder, first: Int, offset: Int): [Todo] + aggregateTodo(filter: TodoFilter): TodoAggregateResult + getUser(username: String!): User + queryUser(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + aggregateUser(filter: UserFilter): UserAggregateResult +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addTodo(input: [AddTodoInput!]!): AddTodoPayload + updateTodo(input: UpdateTodoInput!): UpdateTodoPayload + deleteTodo(filter: TodoFilter!): DeleteTodoPayload + addUser(input: [AddUserInput!]!): AddUserPayload + updateUser(input: UpdateUserInput!): UpdateUserPayload + deleteUser(filter: UserFilter!): DeleteUserPayload +} + diff --git a/graphql/schema/testdata/apolloservice/output/custom-directive.graphql b/graphql/schema/testdata/apolloservice/output/custom-directive.graphql new file mode 100644 index 00000000000..9d9423677d7 --- /dev/null +++ b/graphql/schema/testdata/apolloservice/output/custom-directive.graphql @@ -0,0 +1,359 @@ +####################### +# Input Schema +####################### + +type User @remote { + id: ID! + name: String! +} + +type Car @key(fields: "id") { + id: ID! + name: String! +} + +####################### +# 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 + +input IntRange{ + min: Int! + max: Int! +} + +input FloatRange{ + min: Float! + max: Float! +} + +input Int64Range{ + min: Int64! + max: Int64! +} + +input DateTimeRange{ + min: DateTime! + max: DateTime! +} + +input StringRange{ + min: String! + max: String! +} + +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 + within: WithinFilter +} + +type PointList { + points: [Point!]! +} + +input PointListRef { + points: [PointRef!]! +} + +type Polygon { + coordinates: [PointList!]! +} + +input PolygonRef { + coordinates: [PointListRef!]! +} + +type MultiPolygon { + polygons: [Polygon!]! +} + +input MultiPolygonRef { + polygons: [PolygonRef!]! +} + +input WithinFilter { + polygon: PolygonRef! +} + +input ContainsFilter { + point: PointRef + polygon: PolygonRef +} + +input IntersectsFilter { + polygon: PolygonRef + multiPolygon: MultiPolygonRef +} + +input PolygonGeoFilter { + near: NearFilter + within: WithinFilter + contains: ContainsFilter + intersects: IntersectsFilter +} + +input GenerateQueryParams { + get: Boolean + query: Boolean + password: Boolean + aggregate: Boolean +} + +input GenerateMutationParams { + add: Boolean + update: Boolean + delete: Boolean +} + +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 @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM +directive @cascade(fields: [String]) on FIELD +directive @lambda on FIELD_DEFINITION +directive @cacheControl(maxAge: Int!) on QUERY + +input IntFilter { + eq: Int + in: [Int] + le: Int + lt: Int + ge: Int + gt: Int + between: IntRange +} + +input Int64Filter { + eq: Int64 + in: [Int64] + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 + between: Int64Range +} + +input FloatFilter { + eq: Float + in: [Float] + le: Float + lt: Float + ge: Float + gt: Float + between: FloatRange +} + +input DateTimeFilter { + eq: DateTime + in: [DateTime] + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime + between: DateTimeRange +} + +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 + between: StringRange +} + +input StringHashFilter { + eq: String + in: [String] +} + +####################### +# Generated Types +####################### + +type AddCarPayload { + car(filter: CarFilter, order: CarOrder, first: Int, offset: Int): [Car] + numUids: Int +} + +type CarAggregateResult { + count: Int + nameMin: String + nameMax: String +} + +type DeleteCarPayload { + car(filter: CarFilter, order: CarOrder, first: Int, offset: Int): [Car] + msg: String + numUids: Int +} + +type UpdateCarPayload { + car(filter: CarFilter, order: CarOrder, first: Int, offset: Int): [Car] + numUids: Int +} + +####################### +# Generated Enums +####################### + +enum CarHasFilter { + name +} + +enum CarOrderable { + name +} + +####################### +# Generated Inputs +####################### + +input AddCarInput { + name: String! +} + +input CarFilter { + id: [ID!] + has: CarHasFilter + and: [CarFilter] + or: [CarFilter] + not: CarFilter +} + +input CarOrder { + asc: CarOrderable + desc: CarOrderable + then: CarOrder +} + +input CarPatch { + name: String +} + +input CarRef { + id: ID + name: String +} + +input UpdateCarInput { + filter: CarFilter! + set: CarPatch + remove: CarPatch +} + +####################### +# Generated Query +####################### + +type Query { + getMyFavoriteUsers(id: ID!): [User] + getCar(id: ID!): Car + queryCar(filter: CarFilter, order: CarOrder, first: Int, offset: Int): [Car] + aggregateCar(filter: CarFilter): CarAggregateResult +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addCar(input: [AddCarInput!]!): AddCarPayload + updateCar(input: UpdateCarInput!): UpdateCarPayload + deleteCar(filter: CarFilter!): DeleteCarPayload +} + diff --git a/graphql/schema/testdata/apolloservice/output/extended-types.graphql b/graphql/schema/testdata/apolloservice/output/extended-types.graphql new file mode 100644 index 00000000000..e740f1abacc --- /dev/null +++ b/graphql/schema/testdata/apolloservice/output/extended-types.graphql @@ -0,0 +1,447 @@ +####################### +# Input Schema +####################### + +type Mission @key(fields: "id") { + id: ID! + crew: [Astronaut] + designation: String! + startDate: String + endDate: String +} + +type Astronaut @key(fields: "id") @extends { + id: ID! @external + missions(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + missionsAggregate(filter: MissionFilter): MissionAggregateResult +} + +####################### +# 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 + +input IntRange{ + min: Int! + max: Int! +} + +input FloatRange{ + min: Float! + max: Float! +} + +input Int64Range{ + min: Int64! + max: Int64! +} + +input DateTimeRange{ + min: DateTime! + max: DateTime! +} + +input StringRange{ + min: String! + max: String! +} + +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 + within: WithinFilter +} + +type PointList { + points: [Point!]! +} + +input PointListRef { + points: [PointRef!]! +} + +type Polygon { + coordinates: [PointList!]! +} + +input PolygonRef { + coordinates: [PointListRef!]! +} + +type MultiPolygon { + polygons: [Polygon!]! +} + +input MultiPolygonRef { + polygons: [PolygonRef!]! +} + +input WithinFilter { + polygon: PolygonRef! +} + +input ContainsFilter { + point: PointRef + polygon: PolygonRef +} + +input IntersectsFilter { + polygon: PolygonRef + multiPolygon: MultiPolygonRef +} + +input PolygonGeoFilter { + near: NearFilter + within: WithinFilter + contains: ContainsFilter + intersects: IntersectsFilter +} + +input GenerateQueryParams { + get: Boolean + query: Boolean + password: Boolean + aggregate: Boolean +} + +input GenerateMutationParams { + add: Boolean + update: Boolean + delete: Boolean +} + +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 @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM +directive @cascade(fields: [String]) on FIELD +directive @lambda on FIELD_DEFINITION +directive @cacheControl(maxAge: Int!) on QUERY + +input IntFilter { + eq: Int + in: [Int] + le: Int + lt: Int + ge: Int + gt: Int + between: IntRange +} + +input Int64Filter { + eq: Int64 + in: [Int64] + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 + between: Int64Range +} + +input FloatFilter { + eq: Float + in: [Float] + le: Float + lt: Float + ge: Float + gt: Float + between: FloatRange +} + +input DateTimeFilter { + eq: DateTime + in: [DateTime] + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime + between: DateTimeRange +} + +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 + between: StringRange +} + +input StringHashFilter { + eq: String + in: [String] +} + +####################### +# Generated Types +####################### + +type AddAstronautPayload { + astronaut(filter: AstronautFilter, order: AstronautOrder, first: Int, offset: Int): [Astronaut] + numUids: Int +} + +type AddMissionPayload { + mission(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + numUids: Int +} + +type AstronautAggregateResult { + count: Int + idMin: ID + idMax: ID +} + +type DeleteAstronautPayload { + astronaut(filter: AstronautFilter, order: AstronautOrder, first: Int, offset: Int): [Astronaut] + msg: String + numUids: Int +} + +type DeleteMissionPayload { + mission(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + msg: String + numUids: Int +} + +type MissionAggregateResult { + count: Int + designationMin: String + designationMax: String + startDateMin: String + startDateMax: String + endDateMin: String + endDateMax: String +} + +type UpdateAstronautPayload { + astronaut(filter: AstronautFilter, order: AstronautOrder, first: Int, offset: Int): [Astronaut] + numUids: Int +} + +type UpdateMissionPayload { + mission(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + numUids: Int +} + +####################### +# Generated Enums +####################### + +enum AstronautHasFilter { + missions +} + +enum AstronautOrderable { + id +} + +enum MissionHasFilter { + crew + designation + startDate + endDate +} + +enum MissionOrderable { + designation + startDate + endDate +} + +####################### +# Generated Inputs +####################### + +input AddAstronautInput { + id: ID! + missions: [MissionRef] +} + +input AddMissionInput { + crew: [AstronautRef] + designation: String! + startDate: String + endDate: String +} + +input AstronautFilter { + id: [ID!] + has: AstronautHasFilter + and: [AstronautFilter] + or: [AstronautFilter] + not: AstronautFilter +} + +input AstronautOrder { + asc: AstronautOrderable + desc: AstronautOrderable + then: AstronautOrder +} + +input AstronautPatch { + missions: [MissionRef] +} + +input AstronautRef { + id: ID + missions: [MissionRef] +} + +input MissionFilter { + id: [ID!] + has: MissionHasFilter + and: [MissionFilter] + or: [MissionFilter] + not: MissionFilter +} + +input MissionOrder { + asc: MissionOrderable + desc: MissionOrderable + then: MissionOrder +} + +input MissionPatch { + crew: [AstronautRef] + designation: String + startDate: String + endDate: String +} + +input MissionRef { + id: ID + crew: [AstronautRef] + designation: String + startDate: String + endDate: String +} + +input UpdateAstronautInput { + filter: AstronautFilter! + set: AstronautPatch + remove: AstronautPatch +} + +input UpdateMissionInput { + filter: MissionFilter! + set: MissionPatch + remove: MissionPatch +} + +####################### +# Generated Query +####################### + +type Query { + getMission(id: ID!): Mission + queryMission(filter: MissionFilter, order: MissionOrder, first: Int, offset: Int): [Mission] + aggregateMission(filter: MissionFilter): MissionAggregateResult +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addMission(input: [AddMissionInput!]!): AddMissionPayload + updateMission(input: UpdateMissionInput!): UpdateMissionPayload + deleteMission(filter: MissionFilter!): DeleteMissionPayload + addAstronaut(input: [AddAstronautInput!]!): AddAstronautPayload + updateAstronaut(input: UpdateAstronautInput!): UpdateAstronautPayload + deleteAstronaut(filter: AstronautFilter!): DeleteAstronautPayload +} + diff --git a/graphql/schema/testdata/apolloservice/output/generate-directive.graphql b/graphql/schema/testdata/apolloservice/output/generate-directive.graphql new file mode 100644 index 00000000000..3659d783e75 --- /dev/null +++ b/graphql/schema/testdata/apolloservice/output/generate-directive.graphql @@ -0,0 +1,507 @@ +####################### +# Input Schema +####################### + +interface Character @secret(field: "password") { + id: ID! + name: String! @search(by: [exact]) + friends(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + friendsAggregate(filter: CharacterFilter): CharacterAggregateResult +} + +type Human @secret(field: "password") { + id: ID! + name: String! @search(by: [exact]) + friends(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + totalCredits: Int + friendsAggregate(filter: CharacterFilter): CharacterAggregateResult +} + +type Person @withSubscription { + id: ID! + name: String! +} + +####################### +# 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 + +input IntRange{ + min: Int! + max: Int! +} + +input FloatRange{ + min: Float! + max: Float! +} + +input Int64Range{ + min: Int64! + max: Int64! +} + +input DateTimeRange{ + min: DateTime! + max: DateTime! +} + +input StringRange{ + min: String! + max: String! +} + +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 + within: WithinFilter +} + +type PointList { + points: [Point!]! +} + +input PointListRef { + points: [PointRef!]! +} + +type Polygon { + coordinates: [PointList!]! +} + +input PolygonRef { + coordinates: [PointListRef!]! +} + +type MultiPolygon { + polygons: [Polygon!]! +} + +input MultiPolygonRef { + polygons: [PolygonRef!]! +} + +input WithinFilter { + polygon: PolygonRef! +} + +input ContainsFilter { + point: PointRef + polygon: PolygonRef +} + +input IntersectsFilter { + polygon: PolygonRef + multiPolygon: MultiPolygonRef +} + +input PolygonGeoFilter { + near: NearFilter + within: WithinFilter + contains: ContainsFilter + intersects: IntersectsFilter +} + +input GenerateQueryParams { + get: Boolean + query: Boolean + password: Boolean + aggregate: Boolean +} + +input GenerateMutationParams { + add: Boolean + update: Boolean + delete: Boolean +} + +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 @remote on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM +directive @cascade(fields: [String]) on FIELD +directive @lambda on FIELD_DEFINITION +directive @cacheControl(maxAge: Int!) on QUERY + +input IntFilter { + eq: Int + in: [Int] + le: Int + lt: Int + ge: Int + gt: Int + between: IntRange +} + +input Int64Filter { + eq: Int64 + in: [Int64] + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 + between: Int64Range +} + +input FloatFilter { + eq: Float + in: [Float] + le: Float + lt: Float + ge: Float + gt: Float + between: FloatRange +} + +input DateTimeFilter { + eq: DateTime + in: [DateTime] + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime + between: DateTimeRange +} + +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 + between: StringRange +} + +input StringHashFilter { + eq: String + in: [String] +} + +####################### +# Generated Types +####################### + +type AddHumanPayload { + human(filter: HumanFilter, order: HumanOrder, first: Int, offset: Int): [Human] + numUids: Int +} + +type CharacterAggregateResult { + count: Int + nameMin: String + nameMax: String +} + +type DeleteCharacterPayload { + character(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + msg: String + numUids: Int +} + +type DeleteHumanPayload { + human(filter: HumanFilter, order: HumanOrder, first: Int, offset: Int): [Human] + msg: String + numUids: Int +} + +type HumanAggregateResult { + count: Int + nameMin: String + nameMax: String + totalCreditsMin: Int + totalCreditsMax: Int + totalCreditsSum: Int + totalCreditsAvg: Float +} + +type PersonAggregateResult { + count: Int + nameMin: String + nameMax: String +} + +type UpdateCharacterPayload { + character(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + numUids: Int +} + +type UpdateHumanPayload { + human(filter: HumanFilter, order: HumanOrder, first: Int, offset: Int): [Human] + numUids: Int +} + +type UpdatePersonPayload { + person(filter: PersonFilter, order: PersonOrder, first: Int, offset: Int): [Person] + numUids: Int +} + +####################### +# Generated Enums +####################### + +enum CharacterHasFilter { + name + friends +} + +enum CharacterOrderable { + name +} + +enum HumanHasFilter { + name + friends + totalCredits +} + +enum HumanOrderable { + name + totalCredits +} + +enum PersonHasFilter { + name +} + +enum PersonOrderable { + name +} + +####################### +# Generated Inputs +####################### + +input AddHumanInput { + name: String! + friends: [CharacterRef] + totalCredits: Int + password: String! +} + +input CharacterFilter { + id: [ID!] + name: StringExactFilter + has: CharacterHasFilter + and: [CharacterFilter] + or: [CharacterFilter] + not: CharacterFilter +} + +input CharacterOrder { + asc: CharacterOrderable + desc: CharacterOrderable + then: CharacterOrder +} + +input CharacterPatch { + name: String + friends: [CharacterRef] + password: String +} + +input CharacterRef { + id: ID! +} + +input HumanFilter { + id: [ID!] + name: StringExactFilter + has: HumanHasFilter + and: [HumanFilter] + or: [HumanFilter] + not: HumanFilter +} + +input HumanOrder { + asc: HumanOrderable + desc: HumanOrderable + then: HumanOrder +} + +input HumanPatch { + name: String + friends: [CharacterRef] + totalCredits: Int + password: String +} + +input HumanRef { + id: ID + name: String + friends: [CharacterRef] + totalCredits: Int + password: String +} + +input PersonFilter { + id: [ID!] + has: PersonHasFilter + and: [PersonFilter] + or: [PersonFilter] + not: PersonFilter +} + +input PersonOrder { + asc: PersonOrderable + desc: PersonOrderable + then: PersonOrder +} + +input PersonPatch { + name: String +} + +input PersonRef { + id: ID + name: String +} + +input UpdateCharacterInput { + filter: CharacterFilter! + set: CharacterPatch + remove: CharacterPatch +} + +input UpdateHumanInput { + filter: HumanFilter! + set: HumanPatch + remove: HumanPatch +} + +input UpdatePersonInput { + filter: PersonFilter! + set: PersonPatch + remove: PersonPatch +} + +####################### +# Generated Query +####################### + +type Query { + queryCharacter(filter: CharacterFilter, order: CharacterOrder, first: Int, offset: Int): [Character] + aggregateCharacter(filter: CharacterFilter): CharacterAggregateResult + getHuman(id: ID!): Human + checkHumanPassword(id: ID!, password: String!): Human + queryHuman(filter: HumanFilter, order: HumanOrder, first: Int, offset: Int): [Human] + aggregateHuman(filter: HumanFilter): HumanAggregateResult + queryPerson(filter: PersonFilter, order: PersonOrder, first: Int, offset: Int): [Person] +} + +####################### +# Generated Mutations +####################### + +type Mutation { + updateCharacter(input: UpdateCharacterInput!): UpdateCharacterPayload + deleteCharacter(filter: CharacterFilter!): DeleteCharacterPayload + addHuman(input: [AddHumanInput!]!): AddHumanPayload + updateHuman(input: UpdateHumanInput!): UpdateHumanPayload + deleteHuman(filter: HumanFilter!): DeleteHumanPayload + updatePerson(input: UpdatePersonInput!): UpdatePersonPayload +} + +####################### +# Generated Subscriptions +####################### + +type Subscription { + getHuman(id: ID!): Human + queryHuman(filter: HumanFilter, order: HumanOrder, first: Int, offset: Int): [Human] + aggregateHuman(filter: HumanFilter): HumanAggregateResult + queryPerson(filter: PersonFilter, order: PersonOrder, first: Int, offset: Int): [Person] +} diff --git a/graphql/schema/testdata/schemagen/input/apollo-federation.graphql b/graphql/schema/testdata/schemagen/input/apollo-federation.graphql new file mode 100644 index 00000000000..c6f4d14504e --- /dev/null +++ b/graphql/schema/testdata/schemagen/input/apollo-federation.graphql @@ -0,0 +1,32 @@ +extend type Product @key(fields: "id") { + id: ID! @external + name: String! @external + reviews: [Reviews] +} + +type Reviews @key(fields: "id") { + id: ID! + review: String! +} + +type Student @key(fields: "id"){ + id: ID! + name: String! + age: Int! +} + +type School @key(fields: "id"){ + id: ID! + students: [Student] +} + +extend type User @key(fields: "name") { + id: ID! @external + name: String! @id @external + reviews: [Reviews] +} + +type Country { + code: String! @id + name: String! +} \ No newline at end of file diff --git a/graphql/schema/testdata/schemagen/output/apollo-federation.graphql b/graphql/schema/testdata/schemagen/output/apollo-federation.graphql new file mode 100644 index 00000000000..08531e5ecde --- /dev/null +++ b/graphql/schema/testdata/schemagen/output/apollo-federation.graphql @@ -0,0 +1,763 @@ +####################### +# Input Schema +####################### + +type Reviews @key(fields: "id") { + id: ID! + review: String! +} + +type Student @key(fields: "id") { + id: ID! + name: String! + age: Int! +} + +type School @key(fields: "id") { + id: ID! + students(filter: StudentFilter, order: StudentOrder, first: Int, offset: Int): [Student] + studentsAggregate(filter: StudentFilter): StudentAggregateResult +} + +type Country { + code: String! @id + name: String! +} + +type Product @key(fields: "id") @extends { + id: ID! @external + name: String! @external + reviews(filter: ReviewsFilter, order: ReviewsOrder, first: Int, offset: Int): [Reviews] + reviewsAggregate(filter: ReviewsFilter): ReviewsAggregateResult +} + +type User @key(fields: "name") @extends { + id: ID! @external + name: String! @id @external + reviews(filter: ReviewsFilter, order: ReviewsOrder, first: Int, offset: Int): [Reviews] + reviewsAggregate(filter: ReviewsFilter): ReviewsAggregateResult +} + +####################### +# 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 + +input IntRange{ + min: Int! + max: Int! +} + +input FloatRange{ + min: Float! + max: Float! +} + +input Int64Range{ + min: Int64! + max: Int64! +} + +input DateTimeRange{ + min: DateTime! + max: DateTime! +} + +input StringRange{ + min: String! + max: String! +} + +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 + within: WithinFilter +} + +type PointList { + points: [Point!]! +} + +input PointListRef { + points: [PointRef!]! +} + +type Polygon { + coordinates: [PointList!]! +} + +input PolygonRef { + coordinates: [PointListRef!]! +} + +type MultiPolygon { + polygons: [Polygon!]! +} + +input MultiPolygonRef { + polygons: [PolygonRef!]! +} + +input WithinFilter { + polygon: PolygonRef! +} + +input ContainsFilter { + point: PointRef + polygon: PolygonRef +} + +input IntersectsFilter { + polygon: PolygonRef + multiPolygon: MultiPolygonRef +} + +input PolygonGeoFilter { + near: NearFilter + within: WithinFilter + contains: ContainsFilter + intersects: IntersectsFilter +} + +input GenerateQueryParams { + get: Boolean + query: Boolean + password: Boolean + aggregate: Boolean +} + +input GenerateMutationParams { + add: Boolean + update: Boolean + delete: Boolean +} + +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( + password: AuthRule + query: AuthRule, + add: AuthRule, + update: AuthRule, + delete: AuthRule) on OBJECT | INTERFACE +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 +directive @cacheControl(maxAge: Int!) on QUERY +directive @generate( + query: GenerateQueryParams, + mutation: GenerateMutationParams, + subscription: Boolean) on OBJECT | INTERFACE + +input IntFilter { + eq: Int + in: [Int] + le: Int + lt: Int + ge: Int + gt: Int + between: IntRange +} + +input Int64Filter { + eq: Int64 + in: [Int64] + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 + between: Int64Range +} + +input FloatFilter { + eq: Float + in: [Float] + le: Float + lt: Float + ge: Float + gt: Float + between: FloatRange +} + +input DateTimeFilter { + eq: DateTime + in: [DateTime] + le: DateTime + lt: DateTime + ge: DateTime + gt: DateTime + between: DateTimeRange +} + +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 + between: StringRange +} + +input StringHashFilter { + eq: String + in: [String] +} + +####################### +# Extended Apollo Definitions +####################### +union _Entity = Reviews | Student | School | Product | User + +scalar _Any +scalar _FieldSet + +type _Service { + sdl: String +} + +directive @external on FIELD_DEFINITION +directive @key(fields: _FieldSet!) on OBJECT | INTERFACE +directive @extends on OBJECT | INTERFACE + +####################### +# Generated Types +####################### + +type AddCountryPayload { + country(filter: CountryFilter, order: CountryOrder, first: Int, offset: Int): [Country] + numUids: Int +} + +type AddProductPayload { + product(filter: ProductFilter, order: ProductOrder, first: Int, offset: Int): [Product] + numUids: Int +} + +type AddReviewsPayload { + reviews(filter: ReviewsFilter, order: ReviewsOrder, first: Int, offset: Int): [Reviews] + numUids: Int +} + +type AddSchoolPayload { + school(filter: SchoolFilter, first: Int, offset: Int): [School] + numUids: Int +} + +type AddStudentPayload { + student(filter: StudentFilter, order: StudentOrder, first: Int, offset: Int): [Student] + numUids: Int +} + +type AddUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + numUids: Int +} + +type CountryAggregateResult { + count: Int + codeMin: String + codeMax: String + nameMin: String + nameMax: String +} + +type DeleteCountryPayload { + country(filter: CountryFilter, order: CountryOrder, first: Int, offset: Int): [Country] + msg: String + numUids: Int +} + +type DeleteProductPayload { + product(filter: ProductFilter, order: ProductOrder, first: Int, offset: Int): [Product] + msg: String + numUids: Int +} + +type DeleteReviewsPayload { + reviews(filter: ReviewsFilter, order: ReviewsOrder, first: Int, offset: Int): [Reviews] + msg: String + numUids: Int +} + +type DeleteSchoolPayload { + school(filter: SchoolFilter, first: Int, offset: Int): [School] + msg: String + numUids: Int +} + +type DeleteStudentPayload { + student(filter: StudentFilter, order: StudentOrder, first: Int, offset: Int): [Student] + msg: String + numUids: Int +} + +type DeleteUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + msg: String + numUids: Int +} + +type ProductAggregateResult { + count: Int + idMin: ID + idMax: ID +} + +type ReviewsAggregateResult { + count: Int + reviewMin: String + reviewMax: String +} + +type SchoolAggregateResult { + count: Int +} + +type StudentAggregateResult { + count: Int + nameMin: String + nameMax: String + ageMin: Int + ageMax: Int + ageSum: Int + ageAvg: Float +} + +type UpdateCountryPayload { + country(filter: CountryFilter, order: CountryOrder, first: Int, offset: Int): [Country] + numUids: Int +} + +type UpdateProductPayload { + product(filter: ProductFilter, order: ProductOrder, first: Int, offset: Int): [Product] + numUids: Int +} + +type UpdateReviewsPayload { + reviews(filter: ReviewsFilter, order: ReviewsOrder, first: Int, offset: Int): [Reviews] + numUids: Int +} + +type UpdateSchoolPayload { + school(filter: SchoolFilter, first: Int, offset: Int): [School] + numUids: Int +} + +type UpdateStudentPayload { + student(filter: StudentFilter, order: StudentOrder, first: Int, offset: Int): [Student] + numUids: Int +} + +type UpdateUserPayload { + user(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + numUids: Int +} + +type UserAggregateResult { + count: Int + nameMin: String + nameMax: String +} + +####################### +# Generated Enums +####################### + +enum CountryHasFilter { + code + name +} + +enum CountryOrderable { + code + name +} + +enum ProductHasFilter { + reviews +} + +enum ProductOrderable { + id +} + +enum ReviewsHasFilter { + review +} + +enum ReviewsOrderable { + review +} + +enum SchoolHasFilter { + students +} + +enum StudentHasFilter { + name + age +} + +enum StudentOrderable { + name + age +} + +enum UserHasFilter { + name + reviews +} + +enum UserOrderable { + name +} + +####################### +# Generated Inputs +####################### + +input AddCountryInput { + code: String! + name: String! +} + +input AddProductInput { + id: ID! + reviews: [ReviewsRef] +} + +input AddReviewsInput { + review: String! +} + +input AddSchoolInput { + students: [StudentRef] +} + +input AddStudentInput { + name: String! + age: Int! +} + +input AddUserInput { + name: String! + reviews: [ReviewsRef] +} + +input CountryFilter { + code: StringHashFilter + has: CountryHasFilter + and: [CountryFilter] + or: [CountryFilter] + not: CountryFilter +} + +input CountryOrder { + asc: CountryOrderable + desc: CountryOrderable + then: CountryOrder +} + +input CountryPatch { + name: String +} + +input CountryRef { + code: String + name: String +} + +input ProductFilter { + id: [ID!] + has: ProductHasFilter + and: [ProductFilter] + or: [ProductFilter] + not: ProductFilter +} + +input ProductOrder { + asc: ProductOrderable + desc: ProductOrderable + then: ProductOrder +} + +input ProductPatch { + reviews: [ReviewsRef] +} + +input ProductRef { + id: ID + reviews: [ReviewsRef] +} + +input ReviewsFilter { + id: [ID!] + has: ReviewsHasFilter + and: [ReviewsFilter] + or: [ReviewsFilter] + not: ReviewsFilter +} + +input ReviewsOrder { + asc: ReviewsOrderable + desc: ReviewsOrderable + then: ReviewsOrder +} + +input ReviewsPatch { + review: String +} + +input ReviewsRef { + id: ID + review: String +} + +input SchoolFilter { + id: [ID!] + has: SchoolHasFilter + and: [SchoolFilter] + or: [SchoolFilter] + not: SchoolFilter +} + +input SchoolPatch { + students: [StudentRef] +} + +input SchoolRef { + id: ID + students: [StudentRef] +} + +input StudentFilter { + id: [ID!] + has: StudentHasFilter + and: [StudentFilter] + or: [StudentFilter] + not: StudentFilter +} + +input StudentOrder { + asc: StudentOrderable + desc: StudentOrderable + then: StudentOrder +} + +input StudentPatch { + name: String + age: Int +} + +input StudentRef { + id: ID + name: String + age: Int +} + +input UpdateCountryInput { + filter: CountryFilter! + set: CountryPatch + remove: CountryPatch +} + +input UpdateProductInput { + filter: ProductFilter! + set: ProductPatch + remove: ProductPatch +} + +input UpdateReviewsInput { + filter: ReviewsFilter! + set: ReviewsPatch + remove: ReviewsPatch +} + +input UpdateSchoolInput { + filter: SchoolFilter! + set: SchoolPatch + remove: SchoolPatch +} + +input UpdateStudentInput { + filter: StudentFilter! + set: StudentPatch + remove: StudentPatch +} + +input UpdateUserInput { + filter: UserFilter! + set: UserPatch + remove: UserPatch +} + +input UserFilter { + name: StringHashFilter + has: UserHasFilter + and: [UserFilter] + or: [UserFilter] + not: UserFilter +} + +input UserOrder { + asc: UserOrderable + desc: UserOrderable + then: UserOrder +} + +input UserPatch { + reviews: [ReviewsRef] +} + +input UserRef { + name: String + reviews: [ReviewsRef] +} + +####################### +# Generated Query +####################### + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + getReviews(id: ID!): Reviews + queryReviews(filter: ReviewsFilter, order: ReviewsOrder, first: Int, offset: Int): [Reviews] + aggregateReviews(filter: ReviewsFilter): ReviewsAggregateResult + getStudent(id: ID!): Student + queryStudent(filter: StudentFilter, order: StudentOrder, first: Int, offset: Int): [Student] + aggregateStudent(filter: StudentFilter): StudentAggregateResult + getSchool(id: ID!): School + querySchool(filter: SchoolFilter, first: Int, offset: Int): [School] + aggregateSchool(filter: SchoolFilter): SchoolAggregateResult + getCountry(code: String!): Country + queryCountry(filter: CountryFilter, order: CountryOrder, first: Int, offset: Int): [Country] + aggregateCountry(filter: CountryFilter): CountryAggregateResult + getProduct(id: ID!): Product + queryProduct(filter: ProductFilter, order: ProductOrder, first: Int, offset: Int): [Product] + aggregateProduct(filter: ProductFilter): ProductAggregateResult + getUser(name: String!): User + queryUser(filter: UserFilter, order: UserOrder, first: Int, offset: Int): [User] + aggregateUser(filter: UserFilter): UserAggregateResult +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addReviews(input: [AddReviewsInput!]!): AddReviewsPayload + updateReviews(input: UpdateReviewsInput!): UpdateReviewsPayload + deleteReviews(filter: ReviewsFilter!): DeleteReviewsPayload + addStudent(input: [AddStudentInput!]!): AddStudentPayload + updateStudent(input: UpdateStudentInput!): UpdateStudentPayload + deleteStudent(filter: StudentFilter!): DeleteStudentPayload + addSchool(input: [AddSchoolInput!]!): AddSchoolPayload + updateSchool(input: UpdateSchoolInput!): UpdateSchoolPayload + deleteSchool(filter: SchoolFilter!): DeleteSchoolPayload + addCountry(input: [AddCountryInput!]!): AddCountryPayload + updateCountry(input: UpdateCountryInput!): UpdateCountryPayload + deleteCountry(filter: CountryFilter!): DeleteCountryPayload + addProduct(input: [AddProductInput!]!): AddProductPayload + updateProduct(input: UpdateProductInput!): UpdateProductPayload + deleteProduct(filter: ProductFilter!): DeleteProductPayload + addUser(input: [AddUserInput!]!): AddUserPayload + updateUser(input: UpdateUserInput!): UpdateUserPayload + deleteUser(filter: UserFilter!): DeleteUserPayload +} + diff --git a/graphql/schema/testdata/schemagen/output/interface-with-id-directive-and-ID-field.graphql b/graphql/schema/testdata/schemagen/output/interface-with-id-directive-and-ID-field.graphql index e2ed1bca1aa..a9d8dda318b 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-id-directive-and-ID-field.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-id-directive-and-ID-field.graphql @@ -330,7 +330,7 @@ input StudentPatch { input StudentRef { regNo: ID - rollNo: String @id + rollNo: String } input UpdateStudentInput { 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 879a19fd379..92b73b9a358 100755 --- a/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql +++ b/graphql/schema/testdata/schemagen/output/interface-with-id-directive.graphql @@ -428,7 +428,7 @@ input LibraryItemOrder { } input LibraryItemRef { - refID: String! @id + refID: String! } input LibraryPatch { diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index 0c0ed3bfcf9..18f0547d4a1 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -77,6 +77,7 @@ const ( FilterQuery QueryType = "query" AggregateQuery QueryType = "aggregate" SchemaQuery QueryType = "schema" + EntitiesQuery QueryType = "entities" PasswordQuery QueryType = "checkPassword" HTTPQuery QueryType = "http" DQLQuery QueryType = "dql" @@ -96,6 +97,7 @@ type Schema interface { Operation(r *Request) (Operation, error) Queries(t QueryType) []string Mutations(t MutationType) []string + IsFederated() bool } // An Operation is a single valid GraphQL operation. It contains either @@ -129,6 +131,7 @@ type Field interface { HasCustomDirective() (bool, map[string]FieldDefinition) HasLambdaDirective() bool Type() Type + IsExternal() bool SelectionSet() []Field Location() x.Location DgraphPredicate() string @@ -162,6 +165,8 @@ type Query interface { QueryType() QueryType DQLQuery() string Rename(newName string) + KeyField(typeName string) (string, bool, error) + BuildType(typeName string) Type AuthFor(typ Type, jwtVars map[string]interface{}) Query } @@ -205,6 +210,7 @@ type FieldDefinition interface { Type() Type ParentType() Type IsID() bool + IsExternal() bool HasIDDirective() bool Inverse() FieldDefinition WithMemberType(string) FieldDefinition @@ -308,6 +314,10 @@ func (s *schema) Mutations(t MutationType) []string { return result } +func (s *schema) IsFederated() bool { + return s.schema.Types["_Entity"] != nil +} + func (o *operation) IsQuery() bool { return o.op.Operation == ast.Query } @@ -496,7 +506,10 @@ func dgraphMapping(sch *ast.Schema) map[string]map[string]string { } for _, fld := range fields { - if isID(fld) { + // If key field is of ID type but it is an external field, + // then it is stored in Dgraph as string type with Hash index. + // So we need the predicate mapping in this case. + if isID(fld) && !hasExternal(fld) { // We don't need a mapping for the field, as we the dgraph predicate for them is // fixed i.e. uid. continue @@ -583,7 +596,7 @@ func repeatedFieldMappings(s *ast.Schema, dgPreds map[string]map[string]string) repeatedFieldNames := make(map[string]bool) for _, typ := range s.Types { - if !isAbstractKind(typ.Kind) { + if !isAbstractKind(typ.Kind) || isEntityUnion(typ) { continue } @@ -703,6 +716,34 @@ func customAndLambdaMappings(s *ast.Schema) (map[string]map[string]*ast.Directiv return customDirectives, lambdaDirectives } +func hasExtends(def *ast.Definition) bool { + return def.Directives.ForName(apolloExtendsDirective) != nil +} + +func hasExternal(f *ast.FieldDefinition) bool { + return f.Directives.ForName(apolloExternalDirective) != nil +} + +func isEntityUnion(typ *ast.Definition) bool { + return typ.Kind == ast.Union && typ.Name == "_Entity" +} + +func (f *field) IsExternal() bool { + return hasExternal(f.field.Definition) +} + +func (q *query) IsExternal() bool { + return (*field)(q).IsExternal() +} + +func (m *mutation) IsExternal() bool { + return (*field)(m).IsExternal() +} + +func (f *fieldDefinition) IsExternal() bool { + return hasExternal(f.fieldDef) +} + func hasCustomOrLambda(f *ast.FieldDefinition) bool { for _, dir := range f.Directives { if dir.Name == customDirective || dir.Name == lambdaDirective { @@ -712,6 +753,27 @@ func hasCustomOrLambda(f *ast.FieldDefinition) bool { return false } +func isKeyField(f *ast.FieldDefinition, typ *ast.Definition) bool { + keyDirective := typ.Directives.ForName(apolloKeyDirective) + if keyDirective == nil { + return false + } + return f.Name == keyDirective.Arguments[0].Value.Raw +} + +// Filter out those fields which have @external directive and are not @key fields +// in a definition. +func nonExternalAndKeyFields(defn *ast.Definition) ast.FieldList { + fldList := make([]*ast.FieldDefinition, 0) + for _, fld := range defn.Fields { + if hasExternal(fld) && !isKeyField(fld, defn) { + continue + } + fldList = append(fldList, fld) + } + return fldList +} + // buildCustomDirectiveForLambda returns custom directive for the given field to be used for @lambda // The constructed @custom looks like this: // @custom(http: { @@ -1101,6 +1163,16 @@ func (f *field) IDArgValue() (xid *string, uid uint64, err error) { return } +func (q *query) BuildType(typeName string) Type { + t := &ast.Type{} + t.NamedType = typeName + return &astType{ + typ: t, + inSchema: q.op.inSchema, + dgraphPredicate: q.op.inSchema.dgraphPredicate, + } +} + func (f *field) Type() Type { var t *ast.Type if f.field != nil && f.field.Definition != nil { @@ -1448,6 +1520,20 @@ func (q *query) EnumValues() []string { return nil } +func (q *query) KeyField(typeName string) (string, bool, error) { + typ := q.op.inSchema.schema.Types[typeName] + if typ == nil { + return "", false, fmt.Errorf("Type %s not found in the schema", typeName) + } + keyDir := typ.Directives.ForName(apolloKeyDirective) + if keyDir == nil { + return "", false, fmt.Errorf("Type %s doesn't have a key Directive", typeName) + } + fldName := keyDir.Arguments[0].Value.Raw + fldType := typ.Fields.ForName(fldName).Type + return fldName, fldType.Name() == IDType, nil +} + func (m *mutation) ConstructedFor() Type { return (*field)(m).ConstructedFor() } @@ -1568,6 +1654,8 @@ func queryType(name string, custom *ast.Directive) QueryType { return DQLQuery } return HTTPQuery + case name == "_entities": + return EntitiesQuery case strings.HasPrefix(name, "get"): return GetQuery case name == "__schema" || name == "__type" || name == "__typename": @@ -2042,7 +2130,10 @@ func (t *astType) String() string { func (t *astType) IDField() FieldDefinition { def := t.inSchema.schema.Types[t.Name()] - if def.Kind != ast.Object && def.Kind != ast.Interface { + // If the field is of ID type but it is an external field, + // then it is stored in Dgraph as string type with Hash index. + // So the this field is actually not stored as ID type. + if (def.Kind != ast.Object && def.Kind != ast.Interface) || hasExtends(def) { return nil } @@ -2083,8 +2174,11 @@ func (t *astType) XIDField() FieldDefinition { return nil } + // If field is of ID type but it is an external field, + // then it is stored in Dgraph as string type with Hash index. + // So it should be returned as an XID Field. for _, fd := range def.Fields { - if hasIDDirective(fd) { + if hasIDDirective(fd) || (hasExternal(fd) && isID(fd)) { return &fieldDefinition{ fieldDef: fd, inSchema: t.inSchema, diff --git a/graphql/schema/wrappers_test.go b/graphql/schema/wrappers_test.go index e9c61f0c2aa..06f8dd0f9a1 100644 --- a/graphql/schema/wrappers_test.go +++ b/graphql/schema/wrappers_test.go @@ -83,7 +83,7 @@ type Starship { length: Float }` - schHandler, errs := NewHandler(schemaStr, false) + schHandler, errs := NewHandler(schemaStr, false, false) require.NoError(t, errs) sch, err := FromString(schHandler.GQLSchema()) require.NoError(t, err) @@ -268,7 +268,7 @@ func TestDgraphMapping_WithDirectives(t *testing.T) { length: Float }` - schHandler, errs := NewHandler(schemaStr, false) + schHandler, errs := NewHandler(schemaStr, false, false) require.NoError(t, errs) sch, err := FromString(schHandler.GQLSchema()) require.NoError(t, err) @@ -921,7 +921,7 @@ func TestGraphQLQueryInCustomHTTPConfig(t *testing.T) { for _, tcase := range tests { t.Run(tcase.Name, func(t *testing.T) { - schHandler, errs := NewHandler(tcase.GQLSchema, false) + schHandler, errs := NewHandler(tcase.GQLSchema, false, false) require.NoError(t, errs) sch, err := FromString(schHandler.GQLSchema()) require.NoError(t, err) @@ -961,7 +961,7 @@ func TestGraphQLQueryInCustomHTTPConfig(t *testing.T) { c, err := field.CustomHTTPConfig() require.NoError(t, err) - remoteSchemaHandler, errs := NewHandler(tcase.RemoteSchema, false) + remoteSchemaHandler, errs := NewHandler(tcase.RemoteSchema, false, false) require.NoError(t, errs) remoteSchema, err := FromString(remoteSchemaHandler.GQLSchema()) require.NoError(t, err) @@ -1021,7 +1021,7 @@ func TestAllowedHeadersList(t *testing.T) { } for _, test := range tcases { t.Run(test.name, func(t *testing.T) { - schHandler, errs := NewHandler(test.schemaStr, false) + schHandler, errs := NewHandler(test.schemaStr, false, false) require.NoError(t, errs) _, err := FromString(schHandler.GQLSchema()) require.NoError(t, err) @@ -1104,7 +1104,7 @@ func TestCustomLogicHeaders(t *testing.T) { } for _, test := range tcases { t.Run(test.name, func(t *testing.T) { - _, err := NewHandler(test.schemaStr, false) + _, err := NewHandler(test.schemaStr, false, false) require.EqualError(t, err, test.err.Error()) }) } diff --git a/graphql/test/test.go b/graphql/test/test.go index 06324551eae..2bfd240f31c 100644 --- a/graphql/test/test.go +++ b/graphql/test/test.go @@ -56,7 +56,7 @@ func LoadSchemaFromFile(t *testing.T, gqlFile string) schema.Schema { } func LoadSchemaFromString(t *testing.T, sch string) schema.Schema { - handler, err := schema.NewHandler(string(sch), false) + handler, err := schema.NewHandler(string(sch), false, false) requireNoGQLErrors(t, err) return LoadSchema(t, handler.GQLSchema())