Skip to content

Commit 14f378c

Browse files
Merge pull request #98 from samisuteria/samisuteria/federation
Add Federation Support
2 parents 1bdbbc2 + 237ca0b commit 14f378c

23 files changed

+1241
-10
lines changed

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ let package = Package(
1111
],
1212
targets: [
1313
.target(name: "Graphiti", dependencies: ["GraphQL"]),
14-
.testTarget(name: "GraphitiTests", dependencies: ["Graphiti"]),
14+
.testTarget(name: "GraphitiTests", dependencies: ["Graphiti"], resources: [
15+
.copy("FederationTests/GraphQL"),
16+
]),
1517
]
1618
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import GraphQL
2+
3+
let anyType = try! GraphQLScalarType(
4+
name: "_Any",
5+
description: "Scalar representing the JSON form of any type. A __typename field is required.",
6+
serialize: { try map(from: $0) } ,
7+
parseValue: { $0 },
8+
parseLiteral: { ast in
9+
return ast.map
10+
}
11+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Foundation
2+
import GraphQL
3+
import NIO
4+
5+
struct EntityArguments: Codable {
6+
let representations: [Map]
7+
}
8+
9+
struct EntityRepresentation: Codable {
10+
let __typename: String
11+
}
12+
13+
func entityType(_ federatedTypes: [GraphQLObjectType]) -> GraphQLUnionType {
14+
return try! GraphQLUnionType(
15+
name: "_Entity",
16+
description: "Any type that has a federated key definition",
17+
types: federatedTypes
18+
)
19+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import GraphQL
2+
import NIO
3+
4+
public class Key<ObjectType, Resolver, Context, Arguments: Codable>: KeyComponent<ObjectType, Resolver, Context> {
5+
let arguments: [ArgumentComponent<Arguments>]
6+
let resolve: AsyncResolve<Resolver, Context, Arguments, ObjectType?>
7+
8+
override func mapMatchesArguments(_ map: Map, coders: Coders) -> Bool {
9+
let args = try? coders.decoder.decode(Arguments.self, from: map)
10+
return args != nil
11+
}
12+
13+
override func resolveMap(
14+
resolver: Resolver,
15+
context: Context,
16+
map: Map,
17+
eventLoopGroup: EventLoopGroup,
18+
coders: Coders
19+
) throws -> EventLoopFuture<Any?> {
20+
let arguments = try coders.decoder.decode(Arguments.self, from: map)
21+
return try self.resolve(resolver)(context, arguments, eventLoopGroup).map { $0 as Any? }
22+
}
23+
24+
override func validate(againstFields fieldNames: [String], typeProvider: TypeProvider, coders: Coders) throws {
25+
// Ensure that every argument is included in the provided field list
26+
for (name, _) in try arguments(typeProvider: typeProvider, coders: coders) {
27+
if !fieldNames.contains(name) {
28+
throw GraphQLError(message: "Argument name not found in type fields: \(name)")
29+
}
30+
}
31+
}
32+
33+
func arguments(typeProvider: TypeProvider, coders: Coders) throws -> GraphQLArgumentConfigMap {
34+
var map: GraphQLArgumentConfigMap = [:]
35+
36+
for argument in arguments {
37+
let (name, argument) = try argument.argument(typeProvider: typeProvider, coders: coders)
38+
map[name] = argument
39+
}
40+
41+
return map
42+
}
43+
44+
init(
45+
arguments: [ArgumentComponent<Arguments>],
46+
asyncResolve: @escaping AsyncResolve<Resolver, Context, Arguments, ObjectType?>
47+
) {
48+
self.arguments = arguments
49+
self.resolve = asyncResolve
50+
}
51+
52+
convenience init(
53+
arguments: [ArgumentComponent<Arguments>],
54+
simpleAsyncResolve: @escaping SimpleAsyncResolve<
55+
Resolver,
56+
Context,
57+
Arguments,
58+
ObjectType?
59+
>
60+
) {
61+
let asyncResolve: AsyncResolve<Resolver, Context, Arguments, ObjectType?> = { type in
62+
{ context, arguments, group in
63+
// We hop to guarantee that the future will
64+
// return in the same event loop group of the execution.
65+
try simpleAsyncResolve(type)(context, arguments).hop(to: group.next())
66+
}
67+
}
68+
69+
self.init(arguments: arguments, asyncResolve: asyncResolve)
70+
}
71+
72+
convenience init(
73+
arguments: [ArgumentComponent<Arguments>],
74+
syncResolve: @escaping SyncResolve<Resolver, Context, Arguments, ObjectType?>
75+
) {
76+
let asyncResolve: AsyncResolve<Resolver, Context, Arguments, ObjectType?> = { type in
77+
{ context, arguments, group in
78+
let result = try syncResolve(type)(context, arguments)
79+
return group.next().makeSucceededFuture(result)
80+
}
81+
}
82+
83+
self.init(arguments: arguments, asyncResolve: asyncResolve)
84+
}
85+
}
86+
87+
#if compiler(>=5.5) && canImport(_Concurrency)
88+
89+
public extension Key {
90+
@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *)
91+
convenience init(
92+
arguments: [ArgumentComponent<Arguments>],
93+
concurrentResolve: @escaping ConcurrentResolve<
94+
Resolver,
95+
Context,
96+
Arguments,
97+
ObjectType?
98+
>
99+
) {
100+
let asyncResolve: AsyncResolve<Resolver, Context, Arguments, ObjectType?> = { type in
101+
{ context, arguments, eventLoopGroup in
102+
let promise = eventLoopGroup.next().makePromise(of: ObjectType?.self)
103+
promise.completeWithTask {
104+
try await concurrentResolve(type)(context, arguments)
105+
}
106+
return promise.futureResult
107+
}
108+
}
109+
self.init(arguments: arguments, asyncResolve: asyncResolve)
110+
}
111+
}
112+
113+
#endif
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import GraphQL
2+
import NIO
3+
4+
public class KeyComponent<ObjectType, Resolver, Context> {
5+
func mapMatchesArguments(_ map: Map, coders: Coders) -> Bool {
6+
fatalError()
7+
}
8+
9+
func resolveMap(
10+
resolver: Resolver,
11+
context: Context,
12+
map: Map,
13+
eventLoopGroup: EventLoopGroup,
14+
coders: Coders
15+
) throws -> EventLoopFuture<Any?> {
16+
fatalError()
17+
}
18+
19+
func validate(againstFields fieldNames: [String], typeProvider: TypeProvider, coders: Coders) throws {
20+
fatalError()
21+
}
22+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import GraphQL
2+
3+
extension Type {
4+
5+
@discardableResult
6+
/// Define and add the federated key to this type.
7+
///
8+
/// For more information, see https://www.apollographql.com/docs/federation/entities
9+
/// - Parameters:
10+
/// - function: The resolver function used to load this entity based on the key value.
11+
/// - _: The key value. The name of this argument must match a Type field.
12+
/// - Returns: Self for chaining.
13+
public func key<Arguments: Codable>(
14+
at function: @escaping AsyncResolve<Resolver, Context, Arguments, ObjectType?>,
15+
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments>
16+
) -> Self {
17+
keys.append(Key(arguments: [argument()], asyncResolve: function))
18+
return self
19+
}
20+
21+
@discardableResult
22+
/// Define and add the federated key to this type.
23+
///
24+
/// For more information, see https://www.apollographql.com/docs/federation/entities
25+
/// - Parameters:
26+
/// - function: The resolver function used to load this entity based on the key value.
27+
/// - _: The key values. The names of these arguments must match Type fields.
28+
/// - Returns: Self for chaining.
29+
public func key<Arguments: Codable>(
30+
at function: @escaping AsyncResolve<Resolver, Context, Arguments, ObjectType?>,
31+
@ArgumentComponentBuilder<Arguments> _ arguments: ()
32+
-> [ArgumentComponent<Arguments>] = { [] }
33+
) -> Self {
34+
keys.append(Key(arguments: arguments(), asyncResolve: function))
35+
return self
36+
}
37+
38+
@discardableResult
39+
/// Define and add the federated key to this type.
40+
///
41+
/// For more information, see https://www.apollographql.com/docs/federation/entities
42+
/// - Parameters:
43+
/// - function: The resolver function used to load this entity based on the key value.
44+
/// - _: The key value. The name of this argument must match a Type field.
45+
/// - Returns: Self for chaining.
46+
public func key<Arguments: Codable>(
47+
at function: @escaping SimpleAsyncResolve<Resolver, Context, Arguments, ObjectType?>,
48+
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments>
49+
) -> Self {
50+
keys.append(Key(arguments: [argument()], simpleAsyncResolve: function))
51+
return self
52+
}
53+
54+
@discardableResult
55+
/// Define and add the federated key to this type.
56+
///
57+
/// For more information, see https://www.apollographql.com/docs/federation/entities
58+
/// - Parameters:
59+
/// - function: The resolver function used to load this entity based on the key value.
60+
/// - _: The key values. The names of these arguments must match Type fields.
61+
/// - Returns: Self for chaining.
62+
public func key<Arguments: Codable>(
63+
at function: @escaping SimpleAsyncResolve<Resolver, Context, Arguments, ObjectType?>,
64+
@ArgumentComponentBuilder<Arguments> _ arguments: ()
65+
-> [ArgumentComponent<Arguments>] = { [] }
66+
) -> Self {
67+
keys.append(Key(arguments: arguments(), simpleAsyncResolve: function))
68+
return self
69+
}
70+
71+
@discardableResult
72+
/// Define and add the federated key to this type.
73+
///
74+
/// For more information, see https://www.apollographql.com/docs/federation/entities
75+
/// - Parameters:
76+
/// - function: The resolver function used to load this entity based on the key value.
77+
/// - _: The key value. The name of this argument must match a Type field.
78+
/// - Returns: Self for chaining.
79+
public func key<Arguments: Codable>(
80+
at function: @escaping SyncResolve<Resolver, Context, Arguments, ObjectType?>,
81+
@ArgumentComponentBuilder<Arguments> _ arguments: ()
82+
-> [ArgumentComponent<Arguments>] = { [] }
83+
) -> Self {
84+
keys.append(Key(arguments: arguments(), syncResolve: function))
85+
return self
86+
}
87+
88+
@discardableResult
89+
/// Define and add the federated key to this type.
90+
///
91+
/// For more information, see https://www.apollographql.com/docs/federation/entities
92+
/// - Parameters:
93+
/// - function: The resolver function used to load this entity based on the key value.
94+
/// - _: The key values. The names of these arguments must match Type fields.
95+
/// - Returns: Self for chaining.
96+
public func key<Arguments: Codable>(
97+
at function: @escaping SyncResolve<Resolver, Context, Arguments, ObjectType?>,
98+
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments>
99+
) -> Self {
100+
keys.append(Key(arguments: [argument()], syncResolve: function))
101+
return self
102+
}
103+
}
104+
105+
#if compiler(>=5.5) && canImport(_Concurrency)
106+
107+
public extension Type {
108+
@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *)
109+
@discardableResult
110+
/// Define and add the federated key to this type.
111+
///
112+
/// For more information, see https://www.apollographql.com/docs/federation/entities
113+
/// - Parameters:
114+
/// - function: The resolver function used to load this entity based on the key value.
115+
/// - _: The key value. The name of this argument must match a Type field.
116+
/// - Returns: Self for chaining.
117+
func key<Arguments: Codable>(
118+
at function: @escaping ConcurrentResolve<Resolver, Context, Arguments, ObjectType?>,
119+
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments>
120+
) -> Self {
121+
keys.append(Key(arguments: [argument()], concurrentResolve: function))
122+
return self
123+
}
124+
125+
@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *)
126+
@discardableResult
127+
/// Define and add the federated key to this type.
128+
///
129+
/// For more information, see https://www.apollographql.com/docs/federation/entities
130+
/// - Parameters:
131+
/// - function: The resolver function used to load this entity based on the key value.
132+
/// - _: The key values. The names of these arguments must match Type fields.
133+
/// - Returns: Self for chaining.
134+
func key<Arguments: Codable>(
135+
at function: @escaping ConcurrentResolve<Resolver, Context, Arguments, ObjectType?>,
136+
@ArgumentComponentBuilder<Arguments> _ arguments: () -> [ArgumentComponent<Arguments>]
137+
) -> Self {
138+
keys.append(Key(arguments: arguments(), concurrentResolve: function))
139+
return self
140+
}
141+
}
142+
143+
#endif
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import GraphQL
2+
import NIO
3+
4+
let resolveReferenceFieldName = "__resolveReference"
5+
6+
func serviceQuery(for sdl: String) -> GraphQLField {
7+
return GraphQLField(
8+
type: GraphQLNonNull(serviceType),
9+
description: "Return the SDL string for the subschema",
10+
resolve: { source, args, context, eventLoopGroup, info in
11+
let result = Service(sdl: sdl)
12+
return eventLoopGroup.any().makeSucceededFuture(result)
13+
}
14+
)
15+
}
16+
17+
func entitiesQuery(for federatedTypes: [GraphQLObjectType], entityType: GraphQLUnionType, coders: Coders) -> GraphQLField {
18+
return GraphQLField(
19+
type: GraphQLNonNull(GraphQLList(entityType)),
20+
description: "Return all entities matching the provided representations.",
21+
args: ["representations": GraphQLArgument(type: GraphQLList(anyType))],
22+
resolve: { source, args, context, eventLoopGroup, info in
23+
let arguments = try coders.decoder.decode(EntityArguments.self, from: args)
24+
let futures: [EventLoopFuture<Any?>] = try arguments.representations.map { (representationMap: Map) in
25+
let representation = try coders.decoder.decode(
26+
EntityRepresentation.self,
27+
from: representationMap
28+
)
29+
guard let type = federatedTypes.first(where: { value in value.name == representation.__typename }) else {
30+
throw GraphQLError(message: "Federated type not found: \(representation.__typename)")
31+
}
32+
guard let resolve = type.fields[resolveReferenceFieldName]?.resolve else {
33+
throw GraphQLError(
34+
message: "Federated type has no '__resolveReference' field resolver: \(type.name)"
35+
)
36+
}
37+
return try resolve(
38+
source,
39+
representationMap,
40+
context,
41+
eventLoopGroup,
42+
info
43+
)
44+
}
45+
46+
return futures.flatten(on: eventLoopGroup)
47+
.map { $0 as Any? }
48+
}
49+
)
50+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import GraphQL
2+
3+
struct Service: Codable {
4+
let sdl: String
5+
}
6+
7+
let serviceType = try! GraphQLObjectType(
8+
name: "_Service",
9+
description: "Federation service object",
10+
fields: [
11+
"sdl": GraphQLField(type: GraphQLString)
12+
]
13+
)

0 commit comments

Comments
 (0)