diff --git a/example/apollo_federation/README.md b/example/apollo_federation/README.md new file mode 100644 index 00000000..04bc15bd --- /dev/null +++ b/example/apollo_federation/README.md @@ -0,0 +1,35 @@ +# Apollo Federation + +A simple example of integration with apollo federation as subgraph. Tested with Go v1.18, Node.js v16.14.2 and yarn 1.22.18. + +To run this server + +`go run ./example/apollo_federation/subgraph_one/server.go` + +`go run ./example/apollo_federation/subgraph_two/server.go` + +`cd example/apollo_federation/gateway` + +`yarn start` + +and go to localhost:4000 to interact + +Execute the query: + +``` +query { + hello + hi +} +``` + +and you should see a result similar to this: + +```json +{ + "data": { + "hello": "Hello from subgraph one!", + "hi": "Hi from subgraph two!" + } +} +``` diff --git a/example/apollo_federation/gateway/.gitignore b/example/apollo_federation/gateway/.gitignore new file mode 100644 index 00000000..07e6e472 --- /dev/null +++ b/example/apollo_federation/gateway/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/example/apollo_federation/gateway/index.js b/example/apollo_federation/gateway/index.js new file mode 100644 index 00000000..f46b00a1 --- /dev/null +++ b/example/apollo_federation/gateway/index.js @@ -0,0 +1,20 @@ +const { ApolloServer } = require('apollo-server') +const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway'); + +const gateway = new ApolloGateway({ + supergraphSdl: new IntrospectAndCompose({ + subgraphs: [ + { name: 'one', url: 'http://localhost:4001/query' }, + { name: 'two', url: 'http://localhost:4002/query' }, + ], + }), +}); + +const server = new ApolloServer({ + gateway, + subscriptions: false, +}); + +server.listen().then(({ url }) => { + console.log(`Server ready at ${url}`); +}); diff --git a/example/apollo_federation/gateway/package.json b/example/apollo_federation/gateway/package.json new file mode 100644 index 00000000..b2e5af49 --- /dev/null +++ b/example/apollo_federation/gateway/package.json @@ -0,0 +1,14 @@ +{ + "name": "apollo-federation-gateway", + "version": "1.0.0", + "description": "Graphql Federation", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@apollo/gateway": "^0.49.0", + "apollo-server": "^2.21.1", + "graphql": "^15.5.0" + } +} diff --git a/example/apollo_federation/subgraph_one/server.go b/example/apollo_federation/subgraph_one/server.go new file mode 100644 index 00000000..c46634d3 --- /dev/null +++ b/example/apollo_federation/subgraph_one/server.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + "net/http" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" +) + +var schema = ` + schema { + query: Query + } + + type Query { + hello: String! + } +` + +type resolver struct{} + +func (r *resolver) Hello() string { + return "Hello from subgraph one!" +} + +func main() { + opts := []graphql.SchemaOpt{graphql.UseFieldResolvers(), graphql.MaxParallelism(20)} + schema := graphql.MustParseSchema(schema, &resolver{}, opts...) + + http.Handle("/query", &relay.Handler{Schema: schema}) + + log.Fatal(http.ListenAndServe(":4001", nil)) +} diff --git a/example/apollo_federation/subgraph_two/server.go b/example/apollo_federation/subgraph_two/server.go new file mode 100644 index 00000000..03a56813 --- /dev/null +++ b/example/apollo_federation/subgraph_two/server.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + "net/http" + + "github.com/graph-gophers/graphql-go" + "github.com/graph-gophers/graphql-go/relay" +) + +var schema = ` + schema { + query: Query + } + + type Query { + hi: String! + } +` + +type resolver struct{} + +func (r *resolver) Hi() string { + return "Hi from subgraph two!" +} + +func main() { + opts := []graphql.SchemaOpt{graphql.UseFieldResolvers(), graphql.MaxParallelism(20)} + schema := graphql.MustParseSchema(schema, &resolver{}, opts...) + + http.Handle("/query", &relay.Handler{Schema: schema}) + + log.Fatal(http.ListenAndServe(":4002", nil)) +} diff --git a/example/social/introspect.json b/example/social/introspect.json index 330564ec..f344b600 100644 --- a/example/social/introspect.json +++ b/example/social/introspect.json @@ -589,6 +589,33 @@ "name": "User", "possibleTypes": null }, + { + "description": null, + "enumValues": null, + "fields": [ + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "sdl", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + ], + "inputFields": null, + "interfaces": [], + "kind": "OBJECT", + "name": "_Service", + "possibleTypes": null + }, { "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior\nin ways field arguments will not suffice, such as conditionally including or\nskipping a field. Directives provide this by describing additional information\nto the executor.", "enumValues": null, diff --git a/example/starwars/introspect.json b/example/starwars/introspect.json index 2b955ee9..89ed5a3f 100644 --- a/example/starwars/introspect.json +++ b/example/starwars/introspect.json @@ -1231,6 +1231,33 @@ "name": "String", "possibleTypes": null }, + { + "description": null, + "enumValues": null, + "fields": [ + { + "args": [], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "sdl", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + ], + "inputFields": null, + "interfaces": [], + "kind": "OBJECT", + "name": "_Service", + "possibleTypes": null + }, { "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior\nin ways field arguments will not suffice, such as conditionally including or\nskipping a field. Directives provide this by describing additional information\nto the executor.", "enumValues": null, diff --git a/graphql_test.go b/graphql_test.go index c8d9593b..ee1a74a7 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -2144,6 +2144,7 @@ func TestIntrospection(t *testing.T) { { "name": "SearchResult" }, { "name": "Starship" }, { "name": "String" }, + { "name": "_Service" }, { "name": "__Directive" }, { "name": "__DirectiveLocation" }, { "name": "__EnumValue" }, @@ -4326,3 +4327,36 @@ func TestCircularFragmentMaxDepth(t *testing.T) { }, }) } + +func TestQueryService(t *testing.T) { + t.Parallel() + + schemaString := ` + schema { + query: Query + } + + type Query { + hello: String! + }` + + gqltesting.RunTests(t, []*gqltesting.Test{ + { + Schema: graphql.MustParseSchema(schemaString, &helloWorldResolver1{}), + Query: ` + { + _service{ + sdl + } + } + `, + ExpectedResult: fmt.Sprintf(` + { + "_service": { + "sdl": "\n\tschema {\n\t\tquery: Query\n\t}\n\n\ttype Query {\n\t\thello: String!\n\t}" + } + } + `), + }, + }) +} diff --git a/internal/exec/resolvable/meta.go b/internal/exec/resolvable/meta.go index 02d5e262..aafe9acd 100644 --- a/internal/exec/resolvable/meta.go +++ b/internal/exec/resolvable/meta.go @@ -12,8 +12,10 @@ type Meta struct { FieldSchema Field FieldType Field FieldTypename Field + FieldService Field Schema *Object Type *Object + Service *Object } func newMeta(s *types.Schema) *Meta { @@ -32,6 +34,12 @@ func newMeta(s *types.Schema) *Meta { panic(err) } + metaService := s.Types["_Service"].(*types.ObjectTypeDefinition) + sv, err := b.makeObjectExec(metaService.Name, metaService.Fields, nil, false, reflect.TypeOf(&introspection.Service{})) + if err != nil { + panic(err) + } + if err := b.finish(); err != nil { panic(err) } @@ -60,11 +68,21 @@ func newMeta(s *types.Schema) *Meta { TraceLabel: "GraphQL field: __type", } + fieldService := Field{ + FieldDefinition: types.FieldDefinition{ + Name: "_service", + Type: s.Types["_Service"], + }, + TraceLabel: "GraphQL field: _service", + } + return &Meta{ FieldSchema: fieldSchema, FieldTypename: fieldTypename, FieldType: fieldType, + FieldService: fieldService, Schema: so, Type: t, + Service: sv, } } diff --git a/internal/exec/selected/selected.go b/internal/exec/selected/selected.go index 9b96d2b6..868dc1e9 100644 --- a/internal/exec/selected/selected.go +++ b/internal/exec/selected/selected.go @@ -121,6 +121,17 @@ func applySelectionSet(r *Request, s *resolvable.Schema, e *resolvable.Object, s }) } + case "_service": + if !r.DisableIntrospection { + flattenedSels = append(flattenedSels, &SchemaField{ + Field: s.Meta.FieldService, + Alias: field.Alias.Name, + Sels: applySelectionSet(r, s, s.Meta.Service, field.SelectionSet), + Async: true, + FixedResult: reflect.ValueOf(introspection.WrapService(r.Schema)), + }) + } + default: fe := e.Fields[field.Name.Name] diff --git a/internal/schema/meta.go b/internal/schema/meta.go index 9f5bba56..2268b7e7 100644 --- a/internal/schema/meta.go +++ b/internal/schema/meta.go @@ -200,4 +200,8 @@ var metaSrc = ` # Indicates this type is a non-null. ` + "`" + `ofType` + "`" + ` is a valid field. NON_NULL } + + type _Service { + sdl: String! + } ` diff --git a/internal/schema/schema.go b/internal/schema/schema.go index e1c77032..d7791999 100644 --- a/internal/schema/schema.go +++ b/internal/schema/schema.go @@ -161,6 +161,8 @@ func Parse(s *types.Schema, schemaString string, useStringDescriptions bool) err } } + s.SchemaString = schemaString + return nil } diff --git a/internal/validation/validation.go b/internal/validation/validation.go index e3672638..0f07e349 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -327,6 +327,11 @@ func validateSelection(c *opContext, sel types.Selection, t types.NamedType) { }, Type: c.schema.Types["__Type"], } + case "_service": + f = &types.FieldDefinition{ + Name: "_service", + Type: c.schema.Types["_Service"], + } default: f = fields(t).Get(fieldName) if f == nil && t != nil { diff --git a/introspection/introspection.go b/introspection/introspection.go index a0a2fa9b..3df66055 100644 --- a/introspection/introspection.go +++ b/introspection/introspection.go @@ -310,3 +310,16 @@ func (r *Directive) Args() []*InputValue { } return l } + +type Service struct { + schema *types.Schema +} + +// WrapService is only used internally. +func WrapService(schema *types.Schema) *Service { + return &Service{schema} +} + +func (r *Service) SDL() string { + return r.schema.SchemaString +} diff --git a/types/schema.go b/types/schema.go index 06811a97..349c112b 100644 --- a/types/schema.go +++ b/types/schema.go @@ -35,6 +35,7 @@ type Schema struct { Unions []*Union Enums []*EnumTypeDefinition Extensions []*Extension + SchemaString string } func (s *Schema) Resolve(name string) Type {