Skip to content

Commit 67f7173

Browse files
aeramuAlampavelnikolov
authored
Apollo Federation Spec: Fetch service capabilities (#507)
Add basic support for Apollo Federation Co-authored-by: Alam <[email protected]> Co-authored-by: pavelnikolov <[email protected]>
1 parent d48b659 commit 67f7173

File tree

16 files changed

+280
-0
lines changed

16 files changed

+280
-0
lines changed

example/apollo_federation/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Apollo Federation
2+
3+
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.
4+
5+
To run this server
6+
7+
`go run ./example/apollo_federation/subgraph_one/server.go`
8+
9+
`go run ./example/apollo_federation/subgraph_two/server.go`
10+
11+
`cd example/apollo_federation/gateway`
12+
13+
`yarn start`
14+
15+
and go to localhost:4000 to interact
16+
17+
Execute the query:
18+
19+
```
20+
query {
21+
hello
22+
hi
23+
}
24+
```
25+
26+
and you should see a result similar to this:
27+
28+
```json
29+
{
30+
"data": {
31+
"hello": "Hello from subgraph one!",
32+
"hi": "Hi from subgraph two!"
33+
}
34+
}
35+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/node_modules
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const { ApolloServer } = require('apollo-server')
2+
const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');
3+
4+
const gateway = new ApolloGateway({
5+
supergraphSdl: new IntrospectAndCompose({
6+
subgraphs: [
7+
{ name: 'one', url: 'http://localhost:4001/query' },
8+
{ name: 'two', url: 'http://localhost:4002/query' },
9+
],
10+
}),
11+
});
12+
13+
const server = new ApolloServer({
14+
gateway,
15+
subscriptions: false,
16+
});
17+
18+
server.listen().then(({ url }) => {
19+
console.log(`Server ready at ${url}`);
20+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "apollo-federation-gateway",
3+
"version": "1.0.0",
4+
"description": "Graphql Federation",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "node index.js"
8+
},
9+
"dependencies": {
10+
"@apollo/gateway": "^0.49.0",
11+
"apollo-server": "^2.21.1",
12+
"graphql": "^15.5.0"
13+
}
14+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"net/http"
6+
7+
"github.com/graph-gophers/graphql-go"
8+
"github.com/graph-gophers/graphql-go/relay"
9+
)
10+
11+
var schema = `
12+
schema {
13+
query: Query
14+
}
15+
16+
type Query {
17+
hello: String!
18+
}
19+
`
20+
21+
type resolver struct{}
22+
23+
func (r *resolver) Hello() string {
24+
return "Hello from subgraph one!"
25+
}
26+
27+
func main() {
28+
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers(), graphql.MaxParallelism(20)}
29+
schema := graphql.MustParseSchema(schema, &resolver{}, opts...)
30+
31+
http.Handle("/query", &relay.Handler{Schema: schema})
32+
33+
log.Fatal(http.ListenAndServe(":4001", nil))
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"net/http"
6+
7+
"github.com/graph-gophers/graphql-go"
8+
"github.com/graph-gophers/graphql-go/relay"
9+
)
10+
11+
var schema = `
12+
schema {
13+
query: Query
14+
}
15+
16+
type Query {
17+
hi: String!
18+
}
19+
`
20+
21+
type resolver struct{}
22+
23+
func (r *resolver) Hi() string {
24+
return "Hi from subgraph two!"
25+
}
26+
27+
func main() {
28+
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers(), graphql.MaxParallelism(20)}
29+
schema := graphql.MustParseSchema(schema, &resolver{}, opts...)
30+
31+
http.Handle("/query", &relay.Handler{Schema: schema})
32+
33+
log.Fatal(http.ListenAndServe(":4002", nil))
34+
}

example/social/introspect.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,33 @@
589589
"name": "User",
590590
"possibleTypes": null
591591
},
592+
{
593+
"description": null,
594+
"enumValues": null,
595+
"fields": [
596+
{
597+
"args": [],
598+
"deprecationReason": null,
599+
"description": null,
600+
"isDeprecated": false,
601+
"name": "sdl",
602+
"type": {
603+
"kind": "NON_NULL",
604+
"name": null,
605+
"ofType": {
606+
"kind": "SCALAR",
607+
"name": "String",
608+
"ofType": null
609+
}
610+
}
611+
}
612+
],
613+
"inputFields": null,
614+
"interfaces": [],
615+
"kind": "OBJECT",
616+
"name": "_Service",
617+
"possibleTypes": null
618+
},
592619
{
593620
"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.",
594621
"enumValues": null,

example/starwars/introspect.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,33 @@
12311231
"name": "String",
12321232
"possibleTypes": null
12331233
},
1234+
{
1235+
"description": null,
1236+
"enumValues": null,
1237+
"fields": [
1238+
{
1239+
"args": [],
1240+
"deprecationReason": null,
1241+
"description": null,
1242+
"isDeprecated": false,
1243+
"name": "sdl",
1244+
"type": {
1245+
"kind": "NON_NULL",
1246+
"name": null,
1247+
"ofType": {
1248+
"kind": "SCALAR",
1249+
"name": "String",
1250+
"ofType": null
1251+
}
1252+
}
1253+
}
1254+
],
1255+
"inputFields": null,
1256+
"interfaces": [],
1257+
"kind": "OBJECT",
1258+
"name": "_Service",
1259+
"possibleTypes": null
1260+
},
12341261
{
12351262
"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.",
12361263
"enumValues": null,

graphql_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2144,6 +2144,7 @@ func TestIntrospection(t *testing.T) {
21442144
{ "name": "SearchResult" },
21452145
{ "name": "Starship" },
21462146
{ "name": "String" },
2147+
{ "name": "_Service" },
21472148
{ "name": "__Directive" },
21482149
{ "name": "__DirectiveLocation" },
21492150
{ "name": "__EnumValue" },
@@ -4326,3 +4327,36 @@ func TestCircularFragmentMaxDepth(t *testing.T) {
43264327
},
43274328
})
43284329
}
4330+
4331+
func TestQueryService(t *testing.T) {
4332+
t.Parallel()
4333+
4334+
schemaString := `
4335+
schema {
4336+
query: Query
4337+
}
4338+
4339+
type Query {
4340+
hello: String!
4341+
}`
4342+
4343+
gqltesting.RunTests(t, []*gqltesting.Test{
4344+
{
4345+
Schema: graphql.MustParseSchema(schemaString, &helloWorldResolver1{}),
4346+
Query: `
4347+
{
4348+
_service{
4349+
sdl
4350+
}
4351+
}
4352+
`,
4353+
ExpectedResult: fmt.Sprintf(`
4354+
{
4355+
"_service": {
4356+
"sdl": "\n\tschema {\n\t\tquery: Query\n\t}\n\n\ttype Query {\n\t\thello: String!\n\t}"
4357+
}
4358+
}
4359+
`),
4360+
},
4361+
})
4362+
}

internal/exec/resolvable/meta.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ type Meta struct {
1212
FieldSchema Field
1313
FieldType Field
1414
FieldTypename Field
15+
FieldService Field
1516
Schema *Object
1617
Type *Object
18+
Service *Object
1719
}
1820

1921
func newMeta(s *types.Schema) *Meta {
@@ -32,6 +34,12 @@ func newMeta(s *types.Schema) *Meta {
3234
panic(err)
3335
}
3436

37+
metaService := s.Types["_Service"].(*types.ObjectTypeDefinition)
38+
sv, err := b.makeObjectExec(metaService.Name, metaService.Fields, nil, false, reflect.TypeOf(&introspection.Service{}))
39+
if err != nil {
40+
panic(err)
41+
}
42+
3543
if err := b.finish(); err != nil {
3644
panic(err)
3745
}
@@ -60,11 +68,21 @@ func newMeta(s *types.Schema) *Meta {
6068
TraceLabel: "GraphQL field: __type",
6169
}
6270

71+
fieldService := Field{
72+
FieldDefinition: types.FieldDefinition{
73+
Name: "_service",
74+
Type: s.Types["_Service"],
75+
},
76+
TraceLabel: "GraphQL field: _service",
77+
}
78+
6379
return &Meta{
6480
FieldSchema: fieldSchema,
6581
FieldTypename: fieldTypename,
6682
FieldType: fieldType,
83+
FieldService: fieldService,
6784
Schema: so,
6885
Type: t,
86+
Service: sv,
6987
}
7088
}

internal/exec/selected/selected.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,17 @@ func applySelectionSet(r *Request, s *resolvable.Schema, e *resolvable.Object, s
121121
})
122122
}
123123

124+
case "_service":
125+
if !r.DisableIntrospection {
126+
flattenedSels = append(flattenedSels, &SchemaField{
127+
Field: s.Meta.FieldService,
128+
Alias: field.Alias.Name,
129+
Sels: applySelectionSet(r, s, s.Meta.Service, field.SelectionSet),
130+
Async: true,
131+
FixedResult: reflect.ValueOf(introspection.WrapService(r.Schema)),
132+
})
133+
}
134+
124135
default:
125136
fe := e.Fields[field.Name.Name]
126137

internal/schema/meta.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,8 @@ var metaSrc = `
200200
# Indicates this type is a non-null. ` + "`" + `ofType` + "`" + ` is a valid field.
201201
NON_NULL
202202
}
203+
204+
type _Service {
205+
sdl: String!
206+
}
203207
`

internal/schema/schema.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ func Parse(s *types.Schema, schemaString string, useStringDescriptions bool) err
161161
}
162162
}
163163

164+
s.SchemaString = schemaString
165+
164166
return nil
165167
}
166168

internal/validation/validation.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,11 @@ func validateSelection(c *opContext, sel types.Selection, t types.NamedType) {
327327
},
328328
Type: c.schema.Types["__Type"],
329329
}
330+
case "_service":
331+
f = &types.FieldDefinition{
332+
Name: "_service",
333+
Type: c.schema.Types["_Service"],
334+
}
330335
default:
331336
f = fields(t).Get(fieldName)
332337
if f == nil && t != nil {

introspection/introspection.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,16 @@ func (r *Directive) Args() []*InputValue {
310310
}
311311
return l
312312
}
313+
314+
type Service struct {
315+
schema *types.Schema
316+
}
317+
318+
// WrapService is only used internally.
319+
func WrapService(schema *types.Schema) *Service {
320+
return &Service{schema}
321+
}
322+
323+
func (r *Service) SDL() string {
324+
return r.schema.SchemaString
325+
}

types/schema.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Schema struct {
3535
Unions []*Union
3636
Enums []*EnumTypeDefinition
3737
Extensions []*Extension
38+
SchemaString string
3839
}
3940

4041
func (s *Schema) Resolve(name string) Type {

0 commit comments

Comments
 (0)