-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #98 from samisuteria/samisuteria/federation
Add Federation Support
- Loading branch information
Showing
23 changed files
with
1,241 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import GraphQL | ||
|
||
let anyType = try! GraphQLScalarType( | ||
name: "_Any", | ||
description: "Scalar representing the JSON form of any type. A __typename field is required.", | ||
serialize: { try map(from: $0) } , | ||
parseValue: { $0 }, | ||
parseLiteral: { ast in | ||
return ast.map | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import Foundation | ||
import GraphQL | ||
import NIO | ||
|
||
struct EntityArguments: Codable { | ||
let representations: [Map] | ||
} | ||
|
||
struct EntityRepresentation: Codable { | ||
let __typename: String | ||
} | ||
|
||
func entityType(_ federatedTypes: [GraphQLObjectType]) -> GraphQLUnionType { | ||
return try! GraphQLUnionType( | ||
name: "_Entity", | ||
description: "Any type that has a federated key definition", | ||
types: federatedTypes | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import GraphQL | ||
import NIO | ||
|
||
public class Key<ObjectType, Resolver, Context, Arguments: Codable>: KeyComponent<ObjectType, Resolver, Context> { | ||
let arguments: [ArgumentComponent<Arguments>] | ||
let resolve: AsyncResolve<Resolver, Context, Arguments, ObjectType?> | ||
|
||
override func mapMatchesArguments(_ map: Map, coders: Coders) -> Bool { | ||
let args = try? coders.decoder.decode(Arguments.self, from: map) | ||
return args != nil | ||
} | ||
|
||
override func resolveMap( | ||
resolver: Resolver, | ||
context: Context, | ||
map: Map, | ||
eventLoopGroup: EventLoopGroup, | ||
coders: Coders | ||
) throws -> EventLoopFuture<Any?> { | ||
let arguments = try coders.decoder.decode(Arguments.self, from: map) | ||
return try self.resolve(resolver)(context, arguments, eventLoopGroup).map { $0 as Any? } | ||
} | ||
|
||
override func validate(againstFields fieldNames: [String], typeProvider: TypeProvider, coders: Coders) throws { | ||
// Ensure that every argument is included in the provided field list | ||
for (name, _) in try arguments(typeProvider: typeProvider, coders: coders) { | ||
if !fieldNames.contains(name) { | ||
throw GraphQLError(message: "Argument name not found in type fields: \(name)") | ||
} | ||
} | ||
} | ||
|
||
func arguments(typeProvider: TypeProvider, coders: Coders) throws -> GraphQLArgumentConfigMap { | ||
var map: GraphQLArgumentConfigMap = [:] | ||
|
||
for argument in arguments { | ||
let (name, argument) = try argument.argument(typeProvider: typeProvider, coders: coders) | ||
map[name] = argument | ||
} | ||
|
||
return map | ||
} | ||
|
||
init( | ||
arguments: [ArgumentComponent<Arguments>], | ||
asyncResolve: @escaping AsyncResolve<Resolver, Context, Arguments, ObjectType?> | ||
) { | ||
self.arguments = arguments | ||
self.resolve = asyncResolve | ||
} | ||
|
||
convenience init( | ||
arguments: [ArgumentComponent<Arguments>], | ||
simpleAsyncResolve: @escaping SimpleAsyncResolve< | ||
Resolver, | ||
Context, | ||
Arguments, | ||
ObjectType? | ||
> | ||
) { | ||
let asyncResolve: AsyncResolve<Resolver, Context, Arguments, ObjectType?> = { type in | ||
{ context, arguments, group in | ||
// We hop to guarantee that the future will | ||
// return in the same event loop group of the execution. | ||
try simpleAsyncResolve(type)(context, arguments).hop(to: group.next()) | ||
} | ||
} | ||
|
||
self.init(arguments: arguments, asyncResolve: asyncResolve) | ||
} | ||
|
||
convenience init( | ||
arguments: [ArgumentComponent<Arguments>], | ||
syncResolve: @escaping SyncResolve<Resolver, Context, Arguments, ObjectType?> | ||
) { | ||
let asyncResolve: AsyncResolve<Resolver, Context, Arguments, ObjectType?> = { type in | ||
{ context, arguments, group in | ||
let result = try syncResolve(type)(context, arguments) | ||
return group.next().makeSucceededFuture(result) | ||
} | ||
} | ||
|
||
self.init(arguments: arguments, asyncResolve: asyncResolve) | ||
} | ||
} | ||
|
||
#if compiler(>=5.5) && canImport(_Concurrency) | ||
|
||
public extension Key { | ||
@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) | ||
convenience init( | ||
arguments: [ArgumentComponent<Arguments>], | ||
concurrentResolve: @escaping ConcurrentResolve< | ||
Resolver, | ||
Context, | ||
Arguments, | ||
ObjectType? | ||
> | ||
) { | ||
let asyncResolve: AsyncResolve<Resolver, Context, Arguments, ObjectType?> = { type in | ||
{ context, arguments, eventLoopGroup in | ||
let promise = eventLoopGroup.next().makePromise(of: ObjectType?.self) | ||
promise.completeWithTask { | ||
try await concurrentResolve(type)(context, arguments) | ||
} | ||
return promise.futureResult | ||
} | ||
} | ||
self.init(arguments: arguments, asyncResolve: asyncResolve) | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import GraphQL | ||
import NIO | ||
|
||
public class KeyComponent<ObjectType, Resolver, Context> { | ||
func mapMatchesArguments(_ map: Map, coders: Coders) -> Bool { | ||
fatalError() | ||
} | ||
|
||
func resolveMap( | ||
resolver: Resolver, | ||
context: Context, | ||
map: Map, | ||
eventLoopGroup: EventLoopGroup, | ||
coders: Coders | ||
) throws -> EventLoopFuture<Any?> { | ||
fatalError() | ||
} | ||
|
||
func validate(againstFields fieldNames: [String], typeProvider: TypeProvider, coders: Coders) throws { | ||
fatalError() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import GraphQL | ||
|
||
extension Type { | ||
|
||
@discardableResult | ||
/// Define and add the federated key to this type. | ||
/// | ||
/// For more information, see https://www.apollographql.com/docs/federation/entities | ||
/// - Parameters: | ||
/// - function: The resolver function used to load this entity based on the key value. | ||
/// - _: The key value. The name of this argument must match a Type field. | ||
/// - Returns: Self for chaining. | ||
public func key<Arguments: Codable>( | ||
at function: @escaping AsyncResolve<Resolver, Context, Arguments, ObjectType?>, | ||
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments> | ||
) -> Self { | ||
keys.append(Key(arguments: [argument()], asyncResolve: function)) | ||
return self | ||
} | ||
|
||
@discardableResult | ||
/// Define and add the federated key to this type. | ||
/// | ||
/// For more information, see https://www.apollographql.com/docs/federation/entities | ||
/// - Parameters: | ||
/// - function: The resolver function used to load this entity based on the key value. | ||
/// - _: The key values. The names of these arguments must match Type fields. | ||
/// - Returns: Self for chaining. | ||
public func key<Arguments: Codable>( | ||
at function: @escaping AsyncResolve<Resolver, Context, Arguments, ObjectType?>, | ||
@ArgumentComponentBuilder<Arguments> _ arguments: () | ||
-> [ArgumentComponent<Arguments>] = { [] } | ||
) -> Self { | ||
keys.append(Key(arguments: arguments(), asyncResolve: function)) | ||
return self | ||
} | ||
|
||
@discardableResult | ||
/// Define and add the federated key to this type. | ||
/// | ||
/// For more information, see https://www.apollographql.com/docs/federation/entities | ||
/// - Parameters: | ||
/// - function: The resolver function used to load this entity based on the key value. | ||
/// - _: The key value. The name of this argument must match a Type field. | ||
/// - Returns: Self for chaining. | ||
public func key<Arguments: Codable>( | ||
at function: @escaping SimpleAsyncResolve<Resolver, Context, Arguments, ObjectType?>, | ||
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments> | ||
) -> Self { | ||
keys.append(Key(arguments: [argument()], simpleAsyncResolve: function)) | ||
return self | ||
} | ||
|
||
@discardableResult | ||
/// Define and add the federated key to this type. | ||
/// | ||
/// For more information, see https://www.apollographql.com/docs/federation/entities | ||
/// - Parameters: | ||
/// - function: The resolver function used to load this entity based on the key value. | ||
/// - _: The key values. The names of these arguments must match Type fields. | ||
/// - Returns: Self for chaining. | ||
public func key<Arguments: Codable>( | ||
at function: @escaping SimpleAsyncResolve<Resolver, Context, Arguments, ObjectType?>, | ||
@ArgumentComponentBuilder<Arguments> _ arguments: () | ||
-> [ArgumentComponent<Arguments>] = { [] } | ||
) -> Self { | ||
keys.append(Key(arguments: arguments(), simpleAsyncResolve: function)) | ||
return self | ||
} | ||
|
||
@discardableResult | ||
/// Define and add the federated key to this type. | ||
/// | ||
/// For more information, see https://www.apollographql.com/docs/federation/entities | ||
/// - Parameters: | ||
/// - function: The resolver function used to load this entity based on the key value. | ||
/// - _: The key value. The name of this argument must match a Type field. | ||
/// - Returns: Self for chaining. | ||
public func key<Arguments: Codable>( | ||
at function: @escaping SyncResolve<Resolver, Context, Arguments, ObjectType?>, | ||
@ArgumentComponentBuilder<Arguments> _ arguments: () | ||
-> [ArgumentComponent<Arguments>] = { [] } | ||
) -> Self { | ||
keys.append(Key(arguments: arguments(), syncResolve: function)) | ||
return self | ||
} | ||
|
||
@discardableResult | ||
/// Define and add the federated key to this type. | ||
/// | ||
/// For more information, see https://www.apollographql.com/docs/federation/entities | ||
/// - Parameters: | ||
/// - function: The resolver function used to load this entity based on the key value. | ||
/// - _: The key values. The names of these arguments must match Type fields. | ||
/// - Returns: Self for chaining. | ||
public func key<Arguments: Codable>( | ||
at function: @escaping SyncResolve<Resolver, Context, Arguments, ObjectType?>, | ||
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments> | ||
) -> Self { | ||
keys.append(Key(arguments: [argument()], syncResolve: function)) | ||
return self | ||
} | ||
} | ||
|
||
#if compiler(>=5.5) && canImport(_Concurrency) | ||
|
||
public extension Type { | ||
@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) | ||
@discardableResult | ||
/// Define and add the federated key to this type. | ||
/// | ||
/// For more information, see https://www.apollographql.com/docs/federation/entities | ||
/// - Parameters: | ||
/// - function: The resolver function used to load this entity based on the key value. | ||
/// - _: The key value. The name of this argument must match a Type field. | ||
/// - Returns: Self for chaining. | ||
func key<Arguments: Codable>( | ||
at function: @escaping ConcurrentResolve<Resolver, Context, Arguments, ObjectType?>, | ||
@ArgumentComponentBuilder<Arguments> _ argument: () -> ArgumentComponent<Arguments> | ||
) -> Self { | ||
keys.append(Key(arguments: [argument()], concurrentResolve: function)) | ||
return self | ||
} | ||
|
||
@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) | ||
@discardableResult | ||
/// Define and add the federated key to this type. | ||
/// | ||
/// For more information, see https://www.apollographql.com/docs/federation/entities | ||
/// - Parameters: | ||
/// - function: The resolver function used to load this entity based on the key value. | ||
/// - _: The key values. The names of these arguments must match Type fields. | ||
/// - Returns: Self for chaining. | ||
func key<Arguments: Codable>( | ||
at function: @escaping ConcurrentResolve<Resolver, Context, Arguments, ObjectType?>, | ||
@ArgumentComponentBuilder<Arguments> _ arguments: () -> [ArgumentComponent<Arguments>] | ||
) -> Self { | ||
keys.append(Key(arguments: arguments(), concurrentResolve: function)) | ||
return self | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import GraphQL | ||
import NIO | ||
|
||
let resolveReferenceFieldName = "__resolveReference" | ||
|
||
func serviceQuery(for sdl: String) -> GraphQLField { | ||
return GraphQLField( | ||
type: GraphQLNonNull(serviceType), | ||
description: "Return the SDL string for the subschema", | ||
resolve: { source, args, context, eventLoopGroup, info in | ||
let result = Service(sdl: sdl) | ||
return eventLoopGroup.any().makeSucceededFuture(result) | ||
} | ||
) | ||
} | ||
|
||
func entitiesQuery(for federatedTypes: [GraphQLObjectType], entityType: GraphQLUnionType, coders: Coders) -> GraphQLField { | ||
return GraphQLField( | ||
type: GraphQLNonNull(GraphQLList(entityType)), | ||
description: "Return all entities matching the provided representations.", | ||
args: ["representations": GraphQLArgument(type: GraphQLList(anyType))], | ||
resolve: { source, args, context, eventLoopGroup, info in | ||
let arguments = try coders.decoder.decode(EntityArguments.self, from: args) | ||
let futures: [EventLoopFuture<Any?>] = try arguments.representations.map { (representationMap: Map) in | ||
let representation = try coders.decoder.decode( | ||
EntityRepresentation.self, | ||
from: representationMap | ||
) | ||
guard let type = federatedTypes.first(where: { value in value.name == representation.__typename }) else { | ||
throw GraphQLError(message: "Federated type not found: \(representation.__typename)") | ||
} | ||
guard let resolve = type.fields[resolveReferenceFieldName]?.resolve else { | ||
throw GraphQLError( | ||
message: "Federated type has no '__resolveReference' field resolver: \(type.name)" | ||
) | ||
} | ||
return try resolve( | ||
source, | ||
representationMap, | ||
context, | ||
eventLoopGroup, | ||
info | ||
) | ||
} | ||
|
||
return futures.flatten(on: eventLoopGroup) | ||
.map { $0 as Any? } | ||
} | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import GraphQL | ||
|
||
struct Service: Codable { | ||
let sdl: String | ||
} | ||
|
||
let serviceType = try! GraphQLObjectType( | ||
name: "_Service", | ||
description: "Federation service object", | ||
fields: [ | ||
"sdl": GraphQLField(type: GraphQLString) | ||
] | ||
) |
Oops, something went wrong.