diff --git a/graphql/admin/admin.go b/graphql/admin/admin.go index 4fab4b49c70..9e6f49007ec 100644 --- a/graphql/admin/admin.go +++ b/graphql/admin/admin.go @@ -861,6 +861,23 @@ 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, _ := schema.NewHandler(sch, false) + 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/e2e/schema/apolloServiceResponse.graphql b/graphql/e2e/schema/apolloServiceResponse.graphql new file mode 100644 index 00000000000..e4f8ec237c0 --- /dev/null +++ b/graphql/e2e/schema/apolloServiceResponse.graphql @@ -0,0 +1,368 @@ +####################### +# Input Schema +####################### + +type Todo @key(fields: "id") { + id: ID! + title: String! + topic: 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 @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 + le: Int + lt: Int + ge: Int + gt: Int + between: IntRange +} + +input Int64Filter { + eq: Int64 + le: Int64 + lt: Int64 + ge: Int64 + gt: Int64 + between: Int64Range +} + +input FloatFilter { + eq: Float + le: Float + lt: Float + ge: Float + gt: Float + between: FloatRange +} + +input DateTimeFilter { + eq: 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 DeleteTodoPayload { + todo(filter: TodoFilter, order: TodoOrder, first: Int, offset: Int): [Todo] + msg: String + numUids: Int +} + +type TodoAggregateResult { + count: Int + titleMin: String + titleMax: String + topicMin: String + topicMax: String +} + +type UpdateTodoPayload { + todo(filter: TodoFilter, order: TodoOrder, first: Int, offset: Int): [Todo] + numUids: Int +} + +####################### +# Generated Enums +####################### + +enum TodoHasFilter { + title + topic +} + +enum TodoOrderable { + title + topic +} + +####################### +# Generated Inputs +####################### + +input AddTodoInput { + title: String! + topic: String +} + +input TodoFilter { + id: [ID!] + has: TodoHasFilter + and: [TodoFilter] + or: [TodoFilter] + not: TodoFilter +} + +input TodoOrder { + asc: TodoOrderable + desc: TodoOrderable + then: TodoOrder +} + +input TodoPatch { + title: String + topic: String +} + +input TodoRef { + id: ID + title: String + topic: String +} + +input UpdateTodoInput { + filter: TodoFilter! + set: TodoPatch + remove: TodoPatch +} + +####################### +# Generated Query +####################### + +type Query { + getTodo(id: ID!): Todo + queryTodo(filter: TodoFilter, order: TodoOrder, first: Int, offset: Int): [Todo] + aggregateTodo(filter: TodoFilter): TodoAggregateResult +} + +####################### +# Generated Mutations +####################### + +type Mutation { + addTodo(input: [AddTodoInput!]!): AddTodoPayload + updateTodo(input: UpdateTodoInput!): UpdateTodoPayload + deleteTodo(filter: TodoFilter!): DeleteTodoPayload +} + diff --git a/graphql/e2e/schema/schema_test.go b/graphql/e2e/schema/schema_test.go index 430ca79effc..dbfc39ea8ac 100644 --- a/graphql/e2e/schema/schema_test.go +++ b/graphql/e2e/schema/schema_test.go @@ -595,6 +595,36 @@ func TestIntrospection(t *testing.T) { // introspection response or the JSON comparison. Needs deeper looking. } +func TestApolloServiceResolver(t *testing.T) { + schema := ` + type Todo @key(fields: "id") { + id: ID! + title: String! + topic: String + } + ` + 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("apolloServiceResponse.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/schema/schemagen.go b/graphql/schema/schemagen.go index 272222385a2..2b4ee22d118 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 { @@ -72,6 +73,41 @@ func (s *handler) DGSchema() string { return s.dgraphSchema } +// GQLSchemaWithoutApolloExtras return GraphQL schema string +// excluding Apollo extras definitions and Apollo Queries +func (s *handler) GQLSchemaWithoutApolloExtras() string { + typeMapCopy := make(map[string]*ast.Definition) + for typ, defn := range s.completeSchema.Types { + if typ == "_Entity" { + continue + } + typeMapCopy[typ] = defn + } + queryList := make(ast.FieldList, 0) + for _, qry := range s.completeSchema.Query.Fields { + if qry.Name == "_entities" || qry.Name == "_service" { + continue + } + queryList = append(queryList, qry) + } + 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) +} + func parseSecrets(sch string) (map[string]string, *authorization.AuthMeta, error) { m := make(map[string]string) scanner := bufio.NewScanner(strings.NewReader(sch)) diff --git a/graphql/schema/wrappers.go b/graphql/schema/wrappers.go index aeda34bc952..bba9fc151c8 100644 --- a/graphql/schema/wrappers.go +++ b/graphql/schema/wrappers.go @@ -96,6 +96,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 @@ -308,6 +309,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 }