Skip to content

Commit

Permalink
Merge pull request #98 from samisuteria/samisuteria/federation
Browse files Browse the repository at this point in the history
Add Federation Support
  • Loading branch information
NeedleInAJayStack committed Feb 14, 2023
2 parents 1bdbbc2 + 237ca0b commit 14f378c
Show file tree
Hide file tree
Showing 23 changed files with 1,241 additions and 10 deletions.
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ let package = Package(
],
targets: [
.target(name: "Graphiti", dependencies: ["GraphQL"]),
.testTarget(name: "GraphitiTests", dependencies: ["Graphiti"]),
.testTarget(name: "GraphitiTests", dependencies: ["Graphiti"], resources: [
.copy("FederationTests/GraphQL"),
]),
]
)
11 changes: 11 additions & 0 deletions Sources/Graphiti/Federation/Any.swift
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
}
)
19 changes: 19 additions & 0 deletions Sources/Graphiti/Federation/Entity.swift
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
)
}
113 changes: 113 additions & 0 deletions Sources/Graphiti/Federation/Key/Key.swift
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
22 changes: 22 additions & 0 deletions Sources/Graphiti/Federation/Key/KeyComponent.swift
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()
}
}
143 changes: 143 additions & 0 deletions Sources/Graphiti/Federation/Key/Type+Key.swift
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
50 changes: 50 additions & 0 deletions Sources/Graphiti/Federation/Queries.swift
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? }
}
)
}
13 changes: 13 additions & 0 deletions Sources/Graphiti/Federation/Service.swift
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)
]
)
Loading

0 comments on commit 14f378c

Please sign in to comment.