From 8c7cb188c31a814eda16e3dd5db30fcc5c02ec13 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 17 Feb 2021 13:13:38 -0700 Subject: [PATCH 01/36] Adds RxSwift package --- Package.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index b7147de5..fe37e802 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.10.1")), - .package(url: "https://github.com/wickwirew/Runtime.git", .upToNextMinor(from: "2.1.0")) + .package(url: "https://github.com/wickwirew/Runtime.git", .upToNextMinor(from: "2.1.0")), + .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.1.0")) ], targets: [ .target( @@ -16,6 +17,7 @@ let package = Package( dependencies: [ .product(name: "NIO", package: "swift-nio"), .product(name: "Runtime", package: "Runtime"), + .product(name: "RxSwift", package: "RxSwift") ] ), .testTarget(name: "GraphQLTests", dependencies: ["GraphQL"]), From 96edc495e8ef0307304994b9766b44fe1dcfc669 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 17 Feb 2021 13:15:52 -0700 Subject: [PATCH 02/36] Adds execution helper method from JS reference --- Sources/GraphQL/Execution/Execute.swift | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 62acdb25..1a6eb56d 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -1229,3 +1229,26 @@ func getFieldDef( // we know this field exists because we passed validation before execution return parentType.fields[fieldName]! } + +func buildResolveInfo( + context: ExecutionContext, + fieldDef: GraphQLFieldDefinition, + fieldASTs: [Field], + parentType: GraphQLObjectType, + path: IndexPath +) -> GraphQLResolveInfo { + // The resolve function's optional fourth argument is a collection of + // information about the current execution state. + return GraphQLResolveInfo.init( + fieldName: fieldDef.name, + fieldASTs: fieldASTs, + returnType: fieldDef.type, + parentType: parentType, + path: path, + schema: context.schema, + fragments: context.fragments, + rootValue: context.rootValue, + operation: context.operation, + variableValues: context.variableValues + ) +} From 377b321243f1fea6efb9d9f5735ac6e31acfc08a Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 17 Feb 2021 13:16:25 -0700 Subject: [PATCH 03/36] Includes subscription into GraphQL definition file --- Sources/GraphQL/Type/Definition.swift | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 83afd6d8..206221a0 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -395,7 +395,8 @@ func defineFieldMap(name: String, fields: GraphQLFieldMap) throws -> GraphQLFiel description: config.description, deprecationReason: config.deprecationReason, args: try defineArgumentMap(args: config.args), - resolve: config.resolve + resolve: config.resolve, + subscribe: config.subscribe ) fieldMap[name] = field @@ -516,6 +517,7 @@ public struct GraphQLField { public let deprecationReason: String? public let description: String? public let resolve: GraphQLFieldResolve? + public let subscribe: GraphQLFieldResolve? public init( type: GraphQLOutputType, @@ -528,6 +530,7 @@ public struct GraphQLField { self.deprecationReason = deprecationReason self.description = description self.resolve = nil + self.subscribe = nil } public init( @@ -542,6 +545,22 @@ public struct GraphQLField { self.deprecationReason = deprecationReason self.description = description self.resolve = resolve + self.subscribe = nil + } + + public init( + type: GraphQLOutputType, + description: String? = nil, + deprecationReason: String? = nil, + args: GraphQLArgumentConfigMap = [:], + subscribe: GraphQLFieldResolve? + ) { + self.type = type + self.args = args + self.deprecationReason = deprecationReason + self.description = description + self.resolve = nil + self.subscribe = subscribe } public init( @@ -560,6 +579,7 @@ public struct GraphQLField { let result = try resolve(source, args, context, info) return eventLoopGroup.next().makeSucceededFuture(result) } + self.subscribe = nil } } @@ -571,6 +591,7 @@ public final class GraphQLFieldDefinition { public internal(set) var type: GraphQLOutputType public let args: [GraphQLArgumentDefinition] public let resolve: GraphQLFieldResolve? + public let subscribe: GraphQLFieldResolve? public let deprecationReason: String? public let isDeprecated: Bool @@ -580,13 +601,15 @@ public final class GraphQLFieldDefinition { description: String? = nil, deprecationReason: String? = nil, args: [GraphQLArgumentDefinition] = [], - resolve: GraphQLFieldResolve? + resolve: GraphQLFieldResolve?, + subscribe: GraphQLFieldResolve? = nil ) { self.name = name self.description = description self.type = type self.args = args self.resolve = resolve + self.subscribe = subscribe self.deprecationReason = deprecationReason self.isDeprecated = deprecationReason != nil } From 7a4abcf398bbdba7ee77880e4fddb22a8b5b6e60 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 17 Feb 2021 13:17:02 -0700 Subject: [PATCH 04/36] Subscription attempt with custom AsyncIterable --- .../GraphQL/Subscription/SimplePubSub.swift | 67 +++++ Sources/GraphQL/Subscription/Subscribe.swift | 279 ++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 Sources/GraphQL/Subscription/SimplePubSub.swift create mode 100644 Sources/GraphQL/Subscription/Subscribe.swift diff --git a/Sources/GraphQL/Subscription/SimplePubSub.swift b/Sources/GraphQL/Subscription/SimplePubSub.swift new file mode 100644 index 00000000..6fb2867c --- /dev/null +++ b/Sources/GraphQL/Subscription/SimplePubSub.swift @@ -0,0 +1,67 @@ +import NIO + +class SimplePubSub { + let subscribers:[SimplePubSubSubscriber] = [] + + public func emit(event:T) -> Bool { + for subscriber in subscribers { + subscriber.process(event: event) + } + return subscribers.count > 0 + } + + public func getSubscriber() -> SimplePubSubSubscriber { + return SimplePubSubSubscriber() + } +} + +class SimplePubSubSubscriber { + var pullQueue:[EventLoopFuture] = [] + var pushQueue:[EventLoopFuture] = [] + var listening = true + + func process(event:T) { + + } + + + func emptyQueue() { + listening = false +// subscribers.delete(pushValue) // TODO How do we remove this subscriber from the list?? +// for future in pullQueue { // TODO How can we short-circuit the futures in pullQueue +// future.["value":"undefined", "done":"true"]) +// } + pullQueue.removeAll() + pushQueue.removeAll() + } +} + +class SimplePubSubAsyncIterable : AsyncIterable { + let eventLoopGroup:EventLoopGroup + + var pullQueue:[EventLoopFuture] = [] + var pushQueue:[EventLoopFuture] = [] + var listening = true + + init() { + self.eventLoopGroup = MultiThreadedEventLoopGroup.init(numberOfThreads: 1) + } + + func next() -> EventLoopFuture<[String:String]> { + if !listening { + return eventLoopGroup.next().submit { + return ["value":"undefined", "done":"true"] + } + } else if pushQueue.count > 0 { + return eventLoopGroup.next().submit { + return ["value": try! self.pushQueue.removeFirst().wait(), "done":"false"] + } + } +// else { // TODO Figure out why pushQueue is used as a value, but pullQueue is used as a map... +// return pullQueue.last! +// } + return eventLoopGroup.next().submit { // TODO PLACEHOLDER + return ["value":"PLACEHOLDER", "done":"false"] + } + } +} diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift new file mode 100644 index 00000000..aaf0e779 --- /dev/null +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -0,0 +1,279 @@ +import Dispatch +import Runtime +import RxSwift +import NIO + +/** + * Implements the "Subscribe" algorithm described in the GraphQL specification. + * + * Returns a Promise which resolves to either an AsyncIterator (if successful) + * or an ExecutionResult (error). The promise will be rejected if the schema or + * other arguments to this function are invalid, or if the resolved event stream + * is not an async iterable. + * + * If the client-provided arguments to this function do not result in a + * compliant subscription, a GraphQL Response (ExecutionResult) with + * descriptive errors and no data will be returned. + * + * If the source stream could not be created due to faulty subscription + * resolver logic or underlying systems, the promise will resolve to a single + * ExecutionResult containing `errors` and no `data`. + * + * If the operation succeeded, the promise resolves to an AsyncIterator, which + * yields a stream of ExecutionResults representing the response stream. + * + * Accepts either an object with named arguments, or individual arguments. + */ +func subscribe( + queryStrategy: QueryFieldExecutionStrategy, + mutationStrategy: MutationFieldExecutionStrategy, + subscriptionStrategy: SubscriptionFieldExecutionStrategy, + instrumentation: Instrumentation, + schema: GraphQLSchema, + documentAST: Document, + rootValue: Any, + context: Any, + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil, + fieldResolver: GraphQLFieldResolve, + subscribeFieldResolver: GraphQLFieldResolve +) -> EventLoopFuture { // This is either an AsyncIterator or a GraphQLResult + + + let sourceFuture = createSourceEventStream( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + documentAST: documentAST, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName, + subscribeFieldResolver: subscribeFieldResolver + ) + + // For each payload yielded from a subscription, map it over the normal + // GraphQL `execute` function, with `payload` as the rootValue. + // This implements the "MapSourceToResponseEvent" algorithm described in + // the GraphQL specification. The `execute` function provides the + // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the + // "ExecuteQuery" algorithm, for which `execute` is also used. + func mapSourceToResponse(payload:GraphQLResult) -> EventLoopFuture { + return execute( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + documentAST: documentAST, + rootValue: payload, // Make payload the root value + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ) + } + return sourceFuture.flatMap({ (resultOrStream) -> EventLoopFuture in + if resultOrStream is GraphQLResult { // Return the result directly + return eventLoopGroup.next().makeSucceededFuture(resultOrStream) + } else { // We can assume that it's an AsyncIterable + let stream:AsyncIterable! = resultOrStream + return MappedAsyncIterator(from: stream) { payload -> EventLoopFuture in + mapSourceToResponse(payload: payload) + } + } + }) +} + +/** + * Implements the "CreateSourceEventStream" algorithm described in the + * GraphQL specification, resolving the subscription source event stream. + * + * Returns a Promise which resolves to either an AsyncIterable (if successful) + * or an ExecutionResult (error). The promise will be rejected if the schema or + * other arguments to this function are invalid, or if the resolved event stream + * is not an async iterable. + * + * If the client-provided arguments to this function do not result in a + * compliant subscription, a GraphQL Response (ExecutionResult) with + * descriptive errors and no data will be returned. + * + * If the the source stream could not be created due to faulty subscription + * resolver logic or underlying systems, the promise will resolve to a single + * ExecutionResult containing `errors` and no `data`. + * + * If the operation succeeded, the promise resolves to the AsyncIterable for the + * event stream returned by the resolver. + * + * A Source Event Stream represents a sequence of events, each of which triggers + * a GraphQL execution for that event. + * + * This may be useful when hosting the stateful subscription service in a + * different process or machine than the stateless GraphQL execution engine, + * or otherwise separating these two steps. For more on this, see the + * "Supporting Subscriptions at Scale" information in the GraphQL specification. + */ +func createSourceEventStream( + queryStrategy: QueryFieldExecutionStrategy, + mutationStrategy: MutationFieldExecutionStrategy, + subscriptionStrategy: SubscriptionFieldExecutionStrategy, + instrumentation: Instrumentation, + schema: GraphQLSchema, + documentAST: Document, + rootValue: Any, + context: Any, + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil, + subscribeFieldResolver: GraphQLFieldResolve +) -> EventLoopFuture { // This is either an AsyncIterator or a GraphQLResult + + let executeStarted = instrumentation.now + let exeContext: ExecutionContext + + do { + // If a valid context cannot be created due to incorrect arguments, + // this will throw an error. + exeContext = try buildExecutionContext( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + documentAST: documentAST, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + rawVariableValues: variableValues, + operationName: operationName + // TODO shouldn't we be including the subscribeFieldResolver?? + ) + } catch let error as GraphQLError { + instrumentation.operationExecution( + processId: processId(), + threadId: threadId(), + started: executeStarted, + finished: instrumentation.now, + schema: schema, + document: documentAST, + rootValue: rootValue, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operation: nil, + errors: [error], + result: nil + ) + + return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [error])) + } catch { + return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [GraphQLError(error)])) + } + + return try! executeSubscription(context: exeContext, eventLoopGroup: eventLoopGroup) +} + +func executeSubscription( + context: ExecutionContext, + eventLoopGroup: EventLoopGroup +) throws -> EventLoopFuture { // This is either an AsyncIterator or a GraphQLResult + + // Get the first node + let type = try getOperationRootType(schema: context.schema, operation: context.operation) + var inputFields: [String:[Field]] = [:] + var visitedFragmentNames: [String:Bool] = [:] + let fields = try collectFields( + exeContext: context, + runtimeType: type, + selectionSet: context.operation.selectionSet, + fields: &inputFields, + visitedFragmentNames: &visitedFragmentNames + ) + let responseNames = fields.keys + let responseName = responseNames.first! // TODO add error handling here + let fieldNodes = fields[responseName]! + let fieldNode = fieldNodes.first! + + guard let fieldDef = getFieldDef(schema: context.schema, parentType: type, fieldAST: fieldNode) else { + throw GraphQLError.init( + message: "`The subscription field '\(fieldNode.name)' is not defined.`", + nodes: fieldNodes + ) + } + + let path = IndexPath.init().appending(fieldNode.name.value) + let info = buildResolveInfo( + context: context, + fieldDef: fieldDef, + fieldASTs: fieldNodes, + parentType: type, + path: path + ) + + // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. + // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. + + // Build a map of arguments from the field.arguments AST, using the + // variables scope to fulfill any variable references. + let args = try getArgumentValues(argDefs: fieldDef.args, argASTs: fieldNode.arguments, variableValues: context.variableValues) + + // The resolve function's optional third argument is a context value that + // is provided to every resolve function within an execution. It is commonly + // used to represent an authenticated user, or request-specific caches. + let contextValue = context.context + + // Call the `subscribe()` resolver or the default resolver to produce an + // AsyncIterable yielding raw payloads. + let resolve = fieldDef.subscribe ?? fieldDef.resolve ?? defaultResolve + + // Get the resolve func, regardless of if its result is normal + // or abrupt (error). + let result = resolveOrError( + resolve: resolve, + source: context.rootValue, + args: args, + context: contextValue, + eventLoopGroup: eventLoopGroup, + info: info + ) + + return try completeValueCatchingError( + exeContext: context, + returnType: fieldDef.type, + fieldASTs: fieldNodes, + info: info, + path: path, + result: result + ).flatMap { value -> EventLoopFuture in + if let asyncIterable = value as? EventLoopFuture { + return asyncIterable + } else { + context.append(error: GraphQLError(message: "Subscription field must return AsyncIterable.")) + return context.eventLoopGroup.next().makeSucceededFuture(nil) + } + } +} + +protocol AsyncIterable { + associatedtype Item + func next() -> EventLoopFuture +} + +class MappedAsyncIterator : AsyncIterable { + let origIterable: OrigType + let callback: (OrigType.Item) -> Future + + init(from: OrigType, by: @escaping (OrigType.Item) -> Future) { + origIterable = from + callback = by + } + + func next() -> EventLoopFuture { + origIterable.next().flatMap { origResult -> Future in + self.callback(origResult) + } + } +} From 681e213263ece41638790552c83c2cd9b1c441de Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 17 Feb 2021 17:23:47 -0700 Subject: [PATCH 05/36] Adds subscription result and test stub --- .../GraphQL/Subscription/SimplePubSub.swift | 67 -------- Sources/GraphQL/Subscription/Subscribe.swift | 61 +++---- .../Subscription/SubscriptionTests.swift | 157 ++++++++++++++++++ 3 files changed, 179 insertions(+), 106 deletions(-) delete mode 100644 Sources/GraphQL/Subscription/SimplePubSub.swift create mode 100644 Tests/GraphQLTests/Subscription/SubscriptionTests.swift diff --git a/Sources/GraphQL/Subscription/SimplePubSub.swift b/Sources/GraphQL/Subscription/SimplePubSub.swift deleted file mode 100644 index 6fb2867c..00000000 --- a/Sources/GraphQL/Subscription/SimplePubSub.swift +++ /dev/null @@ -1,67 +0,0 @@ -import NIO - -class SimplePubSub { - let subscribers:[SimplePubSubSubscriber] = [] - - public func emit(event:T) -> Bool { - for subscriber in subscribers { - subscriber.process(event: event) - } - return subscribers.count > 0 - } - - public func getSubscriber() -> SimplePubSubSubscriber { - return SimplePubSubSubscriber() - } -} - -class SimplePubSubSubscriber { - var pullQueue:[EventLoopFuture] = [] - var pushQueue:[EventLoopFuture] = [] - var listening = true - - func process(event:T) { - - } - - - func emptyQueue() { - listening = false -// subscribers.delete(pushValue) // TODO How do we remove this subscriber from the list?? -// for future in pullQueue { // TODO How can we short-circuit the futures in pullQueue -// future.["value":"undefined", "done":"true"]) -// } - pullQueue.removeAll() - pushQueue.removeAll() - } -} - -class SimplePubSubAsyncIterable : AsyncIterable { - let eventLoopGroup:EventLoopGroup - - var pullQueue:[EventLoopFuture] = [] - var pushQueue:[EventLoopFuture] = [] - var listening = true - - init() { - self.eventLoopGroup = MultiThreadedEventLoopGroup.init(numberOfThreads: 1) - } - - func next() -> EventLoopFuture<[String:String]> { - if !listening { - return eventLoopGroup.next().submit { - return ["value":"undefined", "done":"true"] - } - } else if pushQueue.count > 0 { - return eventLoopGroup.next().submit { - return ["value": try! self.pushQueue.removeFirst().wait(), "done":"false"] - } - } -// else { // TODO Figure out why pushQueue is used as a value, but pullQueue is used as a map... -// return pullQueue.last! -// } - return eventLoopGroup.next().submit { // TODO PLACEHOLDER - return ["value":"PLACEHOLDER", "done":"false"] - } - } -} diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index aaf0e779..5d1ec528 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -38,7 +38,7 @@ func subscribe( operationName: String? = nil, fieldResolver: GraphQLFieldResolve, subscribeFieldResolver: GraphQLFieldResolve -) -> EventLoopFuture { // This is either an AsyncIterator or a GraphQLResult +) -> EventLoopFuture { let sourceFuture = createSourceEventStream( @@ -77,16 +77,18 @@ func subscribe( operationName: operationName ) } - return sourceFuture.flatMap({ (resultOrStream) -> EventLoopFuture in - if resultOrStream is GraphQLResult { // Return the result directly - return eventLoopGroup.next().makeSucceededFuture(resultOrStream) - } else { // We can assume that it's an AsyncIterable - let stream:AsyncIterable! = resultOrStream - return MappedAsyncIterator(from: stream) { payload -> EventLoopFuture in - mapSourceToResponse(payload: payload) + return sourceFuture.flatMap{ subscriptionResult -> EventLoopFuture in + do { + let subscriptionObserver = try subscriptionResult.get() + let eventObserver = subscriptionObserver.map { eventPayload -> GraphQLResult in + return try! mapSourceToResponse(payload: eventPayload).wait() // TODO Remove this wait } + // TODO Making a future here feels it indicates a mistake... + return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.success(eventObserver)) + } catch let graphQLError as GraphQLError { + return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.failure(graphQLError)) } - }) + } } /** @@ -130,7 +132,7 @@ func createSourceEventStream( variableValues: [String: Map] = [:], operationName: String? = nil, subscribeFieldResolver: GraphQLFieldResolve -) -> EventLoopFuture { // This is either an AsyncIterator or a GraphQLResult +) -> EventLoopFuture { let executeStarted = instrumentation.now let exeContext: ExecutionContext @@ -168,9 +170,9 @@ func createSourceEventStream( result: nil ) - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [error])) + return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.failure(error)) } catch { - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [GraphQLError(error)])) + return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.failure(GraphQLError(error))) } return try! executeSubscription(context: exeContext, eventLoopGroup: eventLoopGroup) @@ -179,7 +181,7 @@ func createSourceEventStream( func executeSubscription( context: ExecutionContext, eventLoopGroup: EventLoopGroup -) throws -> EventLoopFuture { // This is either an AsyncIterator or a GraphQLResult +) throws -> EventLoopFuture { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) @@ -226,7 +228,7 @@ func executeSubscription( let contextValue = context.context // Call the `subscribe()` resolver or the default resolver to produce an - // AsyncIterable yielding raw payloads. + // Observable yielding raw payloads. let resolve = fieldDef.subscribe ?? fieldDef.resolve ?? defaultResolve // Get the resolve func, regardless of if its result is normal @@ -247,33 +249,14 @@ func executeSubscription( info: info, path: path, result: result - ).flatMap { value -> EventLoopFuture in - if let asyncIterable = value as? EventLoopFuture { - return asyncIterable + ).map { value -> SubscriptionResult in + if let observable = value as? Observable { + return SubscriptionResult.success(observable) } else { - context.append(error: GraphQLError(message: "Subscription field must return AsyncIterable.")) - return context.eventLoopGroup.next().makeSucceededFuture(nil) + context.append(error: GraphQLError(message: "Subscription field resolver must return Observable of GraphQLResults.")) + return SubscriptionResult.failure(GraphQLError(message: "Subscription field resolver must return Observable of GraphQLResults.")) } } } -protocol AsyncIterable { - associatedtype Item - func next() -> EventLoopFuture -} - -class MappedAsyncIterator : AsyncIterable { - let origIterable: OrigType - let callback: (OrigType.Item) -> Future - - init(from: OrigType, by: @escaping (OrigType.Item) -> Future) { - origIterable = from - callback = by - } - - func next() -> EventLoopFuture { - origIterable.next().flatMap { origResult -> Future in - self.callback(origResult) - } - } -} +typealias SubscriptionResult = Result, GraphQLError> diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift new file mode 100644 index 00000000..7d7d12f3 --- /dev/null +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -0,0 +1,157 @@ +import XCTest +import NIO +import RxSwift +@testable import GraphQL + +class SubscriptionTests : XCTestCase { + + private func createSubscription( + pubsub:Observable, + schema:GraphQLSchema = EmailSchema, + document:Document = defaultSubscriptionAST + ) -> EventLoopFuture { + + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + + // TODO figure out how to generate the subscription +// return subscribe( +// schema: schema, +// documentAST: document, +// eventLoopGroup: eventLoopGroup +// ) + + // TODO Remove placeholder below + return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.failure(GraphQLError(message:"PLACEHOLDER"))) + } +} + +let defaultSubscriptionAST = try! parse(source: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } +""") + +// MARK: Types +struct Email { + let from:String + let subject:String + let message:String + let unread:Bool +} + +struct Inbox { + let emails:[Email] +} + +struct EmailEvent { + let email:Email + let inbox:Inbox +} + +let emails = [ + Email( + from: "joe@graphql.org", + subject: "Hello", + message: "Hello World", + unread: false + ) +] + +func importantEmail(priority: Int) -> Observable { + let inbox = Inbox(emails: emails) + let emailObs = Observable.from(emails) + let emailEventObs = emailObs.map { email -> EmailEvent in + return EmailEvent(email: email, inbox: inbox) + } + return emailEventObs +} + +// MARK: Schema +let EmailType = try! GraphQLObjectType( + name: "Email", + fields: [ + "from": GraphQLField( + type: GraphQLString + ), + "subject": GraphQLField( + type: GraphQLString + ), + "message": GraphQLField( + type: GraphQLString + ), + "unread": GraphQLField( + type: GraphQLBoolean + ), + ] +) +let InboxType = try! GraphQLObjectType( + name: "Inbox", + fields: [ + "emails": GraphQLField( + type: GraphQLList(EmailType) + ), + "total": GraphQLField( + type: GraphQLInt, + resolve: { inbox, _, _, _ in + (inbox as! Inbox).emails.count + } + ), + // TODO figure out how to do searches +// "unread": GraphQLField( +// type: GraphQLInt, +// resolve: { inbox, _, _, _ in +// (inbox as! InboxType).emails. +// } +// ), + ] +) + +let EmailEventType = try! GraphQLObjectType( + name: "EmailEvent", + fields: [ + "email": GraphQLField( + type: EmailType + ), + "inbox": GraphQLField( + type: InboxType + ) + ] +) + +let EmailSchema = try! GraphQLSchema( + query: try! GraphQLObjectType( + name: "Query", + fields: [ + "inbox": GraphQLField( + type: InboxType + ) + ] + ), + subscription: try! GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + args: [ + "priority": GraphQLArgument( + type: GraphQLInt + ) + ], + resolve: { _, arguments, _, _ in + let priority = arguments["priority"].int! + return importantEmail(priority: priority) + } + // subscribe: subscribeFn // TODO Fill in the subscribe function + ) + ] + ) +) From 220713b38acb8b3a30aab1e6508d44077e8877ea Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 19 Feb 2021 11:20:00 -0700 Subject: [PATCH 06/36] Implements some testing... getting closer --- Sources/GraphQL/Subscription/Subscribe.swift | 46 ++- Sources/GraphQL/Type/Definition.swift | 35 +++ .../Subscription/SubscriptionTests.swift | 262 ++++++++++++++---- 3 files changed, 275 insertions(+), 68 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 5d1ec528..ee5239f0 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -36,8 +36,8 @@ func subscribe( eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil, - fieldResolver: GraphQLFieldResolve, - subscribeFieldResolver: GraphQLFieldResolve + fieldResolver: GraphQLFieldResolve? = nil, + subscribeFieldResolver: GraphQLFieldResolve? = nil ) -> EventLoopFuture { @@ -62,7 +62,7 @@ func subscribe( // the GraphQL specification. The `execute` function provides the // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the // "ExecuteQuery" algorithm, for which `execute` is also used. - func mapSourceToResponse(payload:GraphQLResult) -> EventLoopFuture { + func mapSourceToResponse(payload: Any) -> EventLoopFuture { return execute( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, @@ -131,8 +131,8 @@ func createSourceEventStream( eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil, - subscribeFieldResolver: GraphQLFieldResolve -) -> EventLoopFuture { + subscribeFieldResolver: GraphQLFieldResolve? = nil +) -> EventLoopFuture { let executeStarted = instrumentation.now let exeContext: ExecutionContext @@ -170,9 +170,9 @@ func createSourceEventStream( result: nil ) - return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.failure(error)) + return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult.failure(error)) } catch { - return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.failure(GraphQLError(error))) + return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult.failure(GraphQLError(error))) } return try! executeSubscription(context: exeContext, eventLoopGroup: eventLoopGroup) @@ -181,7 +181,7 @@ func createSourceEventStream( func executeSubscription( context: ExecutionContext, eventLoopGroup: EventLoopGroup -) throws -> EventLoopFuture { +) throws -> EventLoopFuture { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) @@ -241,7 +241,6 @@ func executeSubscription( eventLoopGroup: eventLoopGroup, info: info ) - return try completeValueCatchingError( exeContext: context, returnType: fieldDef.type, @@ -249,14 +248,33 @@ func executeSubscription( info: info, path: path, result: result - ).map { value -> SubscriptionResult in - if let observable = value as? Observable { - return SubscriptionResult.success(observable) + ) + // TODO do we need to create this data map? +// .flatMapThrowing { data -> Any in +// // Translate from raw value completion map into a GraphQLResult +// var dataMap: Map = [:] +// dataMap[fieldDef.name] = try map(from: data) +// var result: GraphQLResult = GraphQLResult(data: dataMap) +// +// if !context.errors.isEmpty { +// result.errors = context.errors +// } +// return result +// } + .map { value -> SourceEventStreamResult in + if !context.errors.isEmpty { + // TODO improve this to return multiple errors if we have them. + return SourceEventStreamResult.failure(context.errors.first!) + } else if value is Observable { + let observable = value as! Observable + return SourceEventStreamResult.success(observable) + } else if let error = value as? GraphQLError { + return SourceEventStreamResult.failure(error) } else { - context.append(error: GraphQLError(message: "Subscription field resolver must return Observable of GraphQLResults.")) - return SubscriptionResult.failure(GraphQLError(message: "Subscription field resolver must return Observable of GraphQLResults.")) + return SourceEventStreamResult.failure(GraphQLError(message: "Subscription field resolver must return Observable of GraphQLResults.")) } } } typealias SubscriptionResult = Result, GraphQLError> +typealias SourceEventStreamResult = Result, GraphQLError> diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 206221a0..08edee79 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -548,6 +548,22 @@ public struct GraphQLField { self.subscribe = nil } + public init( + type: GraphQLOutputType, + description: String? = nil, + deprecationReason: String? = nil, + args: GraphQLArgumentConfigMap = [:], + resolve: GraphQLFieldResolve?, + subscribe: GraphQLFieldResolve? + ) { + self.type = type + self.args = args + self.deprecationReason = deprecationReason + self.description = description + self.resolve = resolve + self.subscribe = subscribe + } + public init( type: GraphQLOutputType, description: String? = nil, @@ -581,6 +597,25 @@ public struct GraphQLField { } self.subscribe = nil } + +// public init( +// type: GraphQLOutputType, +// description: String? = nil, +// deprecationReason: String? = nil, +// args: GraphQLArgumentConfigMap = [:], +// subscribe: GraphQLFieldResolveInput +// ) { +// self.type = type +// self.args = args +// self.deprecationReason = deprecationReason +// self.description = description +// +// self.resolve = nil +// self.subscribe = { source, args, context, eventLoopGroup, info in +// let result = try subscribe(source, args, context, info) +// return eventLoopGroup.next().makeSucceededFuture(result) +// } +// } } public typealias GraphQLFieldDefinitionMap = [String: GraphQLFieldDefinition] diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index 7d7d12f3..ada0b916 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -7,21 +7,192 @@ class SubscriptionTests : XCTestCase { private func createSubscription( pubsub:Observable, - schema:GraphQLSchema = EmailSchema, + schema:GraphQLSchema = emailSchemaWithResolvers(subscribe: nil, resolve: nil), document:Document = defaultSubscriptionAST ) -> EventLoopFuture { let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - // TODO figure out how to generate the subscription -// return subscribe( -// schema: schema, -// documentAST: document, -// eventLoopGroup: eventLoopGroup -// ) - // TODO Remove placeholder below - return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.failure(GraphQLError(message:"PLACEHOLDER"))) + var emails = [ + Email( + from: "joe@graphql.org", + subject: "Hello", + message: "Hello World", + unread: false + ) + ] + + func importantEmail(priority: Int) -> Observable { + let inbox = Inbox(emails: emails) + let emailSubject = PublishSubject() + let emailEventSubject = emailSubject.map { email -> EmailEvent in + emails.append(email) + return EmailEvent(email: email, inbox: inbox) + } + return emailEventSubject + } + + // TODO This seems weird and should probably be an object type + let rootValue:[String:Any] = [ + "inbox": Inbox(emails: emails), + "importantEmail": importantEmail + ] + + return subscribe( + queryStrategy: SerialFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: SerialFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + schema: schema, + documentAST: document, + rootValue: rootValue, + context: Void(), + eventLoopGroup: eventLoopGroup, + variableValues: [:], + operationName: nil + ) + } + + // MARK: Subscription Initialization Phase + + /// accepts multiple subscription fields defined in schema + // TODO Finish up this test + func testAcceptsMultipleSubscriptionFields() throws { + let pubsub = PublishSubject() + let subscriptionTypeMultiple = try GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField (type: EmailEventType), + "notImportantEmail": GraphQLField (type: EmailEventType) + ] + ) + let testSchema = try GraphQLSchema( + query: EmailQueryType, + subscription: subscriptionTypeMultiple + ) + let subscriptionResult = try createSubscription(pubsub: pubsub, schema: testSchema).wait() + switch subscriptionResult { + case .success: + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + case .failure(let error): + throw error + } + } + + + // TODO Not working. I think it's because it's checking the Resolver return against the Schema-defined return type... + func testResolverReturningErrorSchema() throws { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + let schema = emailSchemaWithResolvers( + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(GraphQLError(message: "test error")) + }, + resolve: nil + ) + let document = try parse(source: """ + subscription { + importantEmail + } + """) + let result = try createSourceEventStream( + queryStrategy: SerialFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: SerialFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + schema: schema, + documentAST: document, + rootValue: Void(), + context: Void(), + eventLoopGroup: eventLoopGroup + ).wait() + + switch result { + case .success: + XCTFail() + case .failure(let error): + let expected = GraphQLError(message:"test error") + XCTAssertEqual(expected, error) + } + } + + // TODO Delete me - this is a test to ensure that we are returning the correct thing. + func testDELETEME() throws { + let pubsub = PublishSubject() + let subscriptionType = try GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + args: [ + "priority": GraphQLArgument( + type: GraphQLInt + ) + ], + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(pubsub) + } + ) + ] + ) + let testSchema = emailSchemaWithResolvers( + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(pubsub) + }, + resolve: nil + ) + let subscriptionResult = try createSubscription(pubsub: pubsub, schema: testSchema).wait() + switch subscriptionResult { + case .success: + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + case .failure(let error): + throw error + } + } + + // Working!!! + func testResolverThrowingErrorSchema() throws { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + let schema = emailSchemaWithResolvers( + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + throw GraphQLError(message: "test error") + }, + resolve: nil + ) + let document = try parse(source: """ + subscription { + importantEmail + } + """) + let result = try createSourceEventStream( + queryStrategy: SerialFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: SerialFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + schema: schema, + documentAST: document, + rootValue: Void(), + context: Void(), + eventLoopGroup: eventLoopGroup + ).wait() + + switch result { + case .success(let observable): + XCTFail() + case .failure(let error): + let expected = GraphQLError(message:"test error") + XCTAssertEqual(expected, error) + } } } @@ -57,24 +228,6 @@ struct EmailEvent { let inbox:Inbox } -let emails = [ - Email( - from: "joe@graphql.org", - subject: "Hello", - message: "Hello World", - unread: false - ) -] - -func importantEmail(priority: Int) -> Observable { - let inbox = Inbox(emails: emails) - let emailObs = Observable.from(emails) - let emailEventObs = emailObs.map { email -> EmailEvent in - return EmailEvent(email: email, inbox: inbox) - } - return emailEventObs -} - // MARK: Schema let EmailType = try! GraphQLObjectType( name: "Email", @@ -127,31 +280,32 @@ let EmailEventType = try! GraphQLObjectType( ] ) -let EmailSchema = try! GraphQLSchema( - query: try! GraphQLObjectType( - name: "Query", - fields: [ - "inbox": GraphQLField( - type: InboxType - ) - ] - ), - subscription: try! GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - args: [ - "priority": GraphQLArgument( - type: GraphQLInt - ) - ], - resolve: { _, arguments, _, _ in - let priority = arguments["priority"].int! - return importantEmail(priority: priority) - } - // subscribe: subscribeFn // TODO Fill in the subscribe function - ) - ] - ) +let EmailQueryType = try! GraphQLObjectType( + name: "Query", + fields: [ + "inbox": GraphQLField( + type: InboxType + ) + ] ) + +func emailSchemaWithResolvers(subscribe: GraphQLFieldResolve?, resolve: GraphQLFieldResolve?) -> GraphQLSchema { + return try! GraphQLSchema( + query: EmailQueryType, + subscription: try! GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + args: [ + "priority": GraphQLArgument( + type: GraphQLInt + ) + ], + resolve: resolve, + subscribe: subscribe + ) + ] + ) + ) +} From a52bed80e036416e24f82612a7b4430b1c569fed Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 19 Feb 2021 14:15:31 -0700 Subject: [PATCH 07/36] Fixes Observable type issues with testing --- Sources/GraphQL/Subscription/Subscribe.swift | 47 +++++------- .../Subscription/SubscriptionTests.swift | 71 ++++++++----------- 2 files changed, 46 insertions(+), 72 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index ee5239f0..72de1757 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -135,12 +135,11 @@ func createSourceEventStream( ) -> EventLoopFuture { let executeStarted = instrumentation.now - let exeContext: ExecutionContext do { // If a valid context cannot be created due to incorrect arguments, // this will throw an error. - exeContext = try buildExecutionContext( + let exeContext = try buildExecutionContext( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -154,6 +153,7 @@ func createSourceEventStream( operationName: operationName // TODO shouldn't we be including the subscribeFieldResolver?? ) + return try executeSubscription(context: exeContext, eventLoopGroup: eventLoopGroup) } catch let error as GraphQLError { instrumentation.operationExecution( processId: processId(), @@ -174,8 +174,6 @@ func createSourceEventStream( } catch { return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult.failure(GraphQLError(error))) } - - return try! executeSubscription(context: exeContext, eventLoopGroup: eventLoopGroup) } func executeSubscription( @@ -233,7 +231,7 @@ func executeSubscription( // Get the resolve func, regardless of if its result is normal // or abrupt (error). - let result = resolveOrError( + let resolvedFutureOrError = resolveOrError( resolve: resolve, source: context.rootValue, args: args, @@ -241,37 +239,24 @@ func executeSubscription( eventLoopGroup: eventLoopGroup, info: info ) - return try completeValueCatchingError( - exeContext: context, - returnType: fieldDef.type, - fieldASTs: fieldNodes, - info: info, - path: path, - result: result - ) - // TODO do we need to create this data map? -// .flatMapThrowing { data -> Any in -// // Translate from raw value completion map into a GraphQLResult -// var dataMap: Map = [:] -// dataMap[fieldDef.name] = try map(from: data) -// var result: GraphQLResult = GraphQLResult(data: dataMap) -// -// if !context.errors.isEmpty { -// result.errors = context.errors -// } -// return result -// } - .map { value -> SourceEventStreamResult in + + let resolvedFuture:Future + switch resolvedFutureOrError { + case let .failure(error): + throw error + case let .success(success): + resolvedFuture = success + } + return resolvedFuture.map { resolved -> SourceEventStreamResult in if !context.errors.isEmpty { // TODO improve this to return multiple errors if we have them. return SourceEventStreamResult.failure(context.errors.first!) - } else if value is Observable { - let observable = value as! Observable - return SourceEventStreamResult.success(observable) - } else if let error = value as? GraphQLError { + } else if let error = resolved as? GraphQLError { return SourceEventStreamResult.failure(error) + } else if let observable = resolved as? Observable { + return SourceEventStreamResult.success(observable) } else { - return SourceEventStreamResult.failure(GraphQLError(message: "Subscription field resolver must return Observable of GraphQLResults.")) + return SourceEventStreamResult.failure(GraphQLError(message: "Subscription field resolver must return an Observable")) } } } diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index ada0b916..a5bb63a1 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -6,7 +6,7 @@ import RxSwift class SubscriptionTests : XCTestCase { private func createSubscription( - pubsub:Observable, + pubsub:Observable, schema:GraphQLSchema = emailSchemaWithResolvers(subscribe: nil, resolve: nil), document:Document = defaultSubscriptionAST ) -> EventLoopFuture { @@ -54,12 +54,40 @@ class SubscriptionTests : XCTestCase { ) } + + + // TODO Delete me - this just goes thru the entire pipeline + func testDELETEME() throws { + let pubsub = PublishSubject() + let testSchema = emailSchemaWithResolvers( + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(pubsub) + }, + resolve: nil + ) + let subscriptionResult = try createSubscription(pubsub: pubsub, schema: testSchema).wait() + switch subscriptionResult { + case .success(let subscription): + let subscriber = subscription.subscribe { + print("Event: \($0)") + } + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + case .failure(let error): + throw error + } + } + // MARK: Subscription Initialization Phase /// accepts multiple subscription fields defined in schema // TODO Finish up this test func testAcceptsMultipleSubscriptionFields() throws { - let pubsub = PublishSubject() + let pubsub = PublishSubject() let subscriptionTypeMultiple = try GraphQLObjectType( name: "Subscription", fields: [ @@ -121,45 +149,6 @@ class SubscriptionTests : XCTestCase { } } - // TODO Delete me - this is a test to ensure that we are returning the correct thing. - func testDELETEME() throws { - let pubsub = PublishSubject() - let subscriptionType = try GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - args: [ - "priority": GraphQLArgument( - type: GraphQLInt - ) - ], - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(pubsub) - } - ) - ] - ) - let testSchema = emailSchemaWithResolvers( - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(pubsub) - }, - resolve: nil - ) - let subscriptionResult = try createSubscription(pubsub: pubsub, schema: testSchema).wait() - switch subscriptionResult { - case .success: - pubsub.onNext(Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - case .failure(let error): - throw error - } - } - // Working!!! func testResolverThrowingErrorSchema() throws { let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) From 50dc3a0189c8fcb6bc233f3169b1d1956ff0274d Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 19 Feb 2021 14:38:34 -0700 Subject: [PATCH 08/36] Adds subscription resolver nil handling --- Sources/GraphQL/Subscription/Subscribe.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 72de1757..6c134af9 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -255,8 +255,14 @@ func executeSubscription( return SourceEventStreamResult.failure(error) } else if let observable = resolved as? Observable { return SourceEventStreamResult.success(observable) + } else if resolved == nil { + return SourceEventStreamResult.failure( + GraphQLError(message: "Resolved subscription was nil") + ) } else { - return SourceEventStreamResult.failure(GraphQLError(message: "Subscription field resolver must return an Observable")) + return SourceEventStreamResult.failure( + GraphQLError(message: "Subscription field resolver must return an Observable, not \(Swift.type(of:resolved))") + ) } } } From ec6132f7b0a45674289673efc06faa37115ad365 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 19 Feb 2021 17:02:44 -0700 Subject: [PATCH 09/36] Fixes test schema. Test subscriptions are working! --- Sources/GraphQL/Subscription/Subscribe.swift | 2 +- .../Subscription/SubscriptionTests.swift | 305 +++++++++--------- 2 files changed, 154 insertions(+), 153 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 6c134af9..d33dbaf7 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -227,7 +227,7 @@ func executeSubscription( // Call the `subscribe()` resolver or the default resolver to produce an // Observable yielding raw payloads. - let resolve = fieldDef.subscribe ?? fieldDef.resolve ?? defaultResolve + let resolve = fieldDef.subscribe ?? defaultResolve // Get the resolve func, regardless of if its result is normal // or abrupt (error). diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index a5bb63a1..aaa353d7 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -3,16 +3,15 @@ import NIO import RxSwift @testable import GraphQL + class SubscriptionTests : XCTestCase { + /// Creates a subscription result for the input pub/sub, schema, and AST document private func createSubscription( pubsub:Observable, - schema:GraphQLSchema = emailSchemaWithResolvers(subscribe: nil, resolve: nil), + schema:GraphQLSchema? = nil, document:Document = defaultSubscriptionAST - ) -> EventLoopFuture { - - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - + ) throws -> Observable { var emails = [ Email( @@ -22,167 +21,170 @@ class SubscriptionTests : XCTestCase { unread: false ) ] - - func importantEmail(priority: Int) -> Observable { - let inbox = Inbox(emails: emails) - let emailSubject = PublishSubject() - let emailEventSubject = emailSubject.map { email -> EmailEvent in + + let testSchema = schema ?? emailSchemaWithResolvers( + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(pubsub) + }, + resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + let email = emailAny as! Email emails.append(email) - return EmailEvent(email: email, inbox: inbox) + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: emails) + )) } - return emailEventSubject - } + ) - // TODO This seems weird and should probably be an object type - let rootValue:[String:Any] = [ - "inbox": Inbox(emails: emails), - "importantEmail": importantEmail - ] + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - return subscribe( + let subscriptionResult = try subscribe( queryStrategy: SerialFieldExecutionStrategy(), mutationStrategy: SerialFieldExecutionStrategy(), subscriptionStrategy: SerialFieldExecutionStrategy(), instrumentation: NoOpInstrumentation, - schema: schema, + schema: testSchema, documentAST: document, - rootValue: rootValue, + rootValue: Void(), context: Void(), eventLoopGroup: eventLoopGroup, variableValues: [:], operationName: nil - ) - } - - - - // TODO Delete me - this just goes thru the entire pipeline - func testDELETEME() throws { - let pubsub = PublishSubject() - let testSchema = emailSchemaWithResolvers( - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(pubsub) - }, - resolve: nil - ) - let subscriptionResult = try createSubscription(pubsub: pubsub, schema: testSchema).wait() + ).wait() + switch subscriptionResult { case .success(let subscription): - let subscriber = subscription.subscribe { - print("Event: \($0)") - } - pubsub.onNext(Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) + return subscription case .failure(let error): throw error } } - // MARK: Subscription Initialization Phase - - /// accepts multiple subscription fields defined in schema - // TODO Finish up this test - func testAcceptsMultipleSubscriptionFields() throws { + // MARK: Basic test to see if publishing is working + func testBasic() throws { + let disposeBag = DisposeBag() let pubsub = PublishSubject() - let subscriptionTypeMultiple = try GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField (type: EmailEventType), - "notImportantEmail": GraphQLField (type: EmailEventType) - ] - ) - let testSchema = try GraphQLSchema( - query: EmailQueryType, - subscription: subscriptionTypeMultiple - ) - let subscriptionResult = try createSubscription(pubsub: pubsub, schema: testSchema).wait() - switch subscriptionResult { - case .success: - pubsub.onNext(Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - case .failure(let error): - throw error - } - } - - - // TODO Not working. I think it's because it's checking the Resolver return against the Schema-defined return type... - func testResolverReturningErrorSchema() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - let schema = emailSchemaWithResolvers( - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(GraphQLError(message: "test error")) - }, - resolve: nil - ) - let document = try parse(source: """ - subscription { - importantEmail - } - """) - let result = try createSourceEventStream( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, - schema: schema, - documentAST: document, - rootValue: Void(), - context: Void(), - eventLoopGroup: eventLoopGroup - ).wait() + let subscription = try createSubscription(pubsub: pubsub) - switch result { - case .success: - XCTFail() - case .failure(let error): - let expected = GraphQLError(message:"test error") - XCTAssertEqual(expected, error) - } - } - - // Working!!! - func testResolverThrowingErrorSchema() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - let schema = emailSchemaWithResolvers( - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - throw GraphQLError(message: "test error") - }, - resolve: nil + let expected = GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 2, + "unread": 1 + ], + "email":[ + "subject": "Alright", + "from": "yuzhi@graphql.org" + ] + ]] ) - let document = try parse(source: """ - subscription { - importantEmail - } - """) - let result = try createSourceEventStream( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, - schema: schema, - documentAST: document, - rootValue: Void(), - context: Void(), - eventLoopGroup: eventLoopGroup - ).wait() - - switch result { - case .success(let observable): - XCTFail() - case .failure(let error): - let expected = GraphQLError(message:"test error") - XCTAssertEqual(expected, error) - } + let _ = subscription.subscribe { event in + XCTAssertEqual(event.element, expected) + }.disposed(by: disposeBag) + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) } + + + // MARK: Subscription Initialization Phase + + /// accepts multiple subscription fields defined in schema +// func testAcceptsMultipleSubscriptionFields() throws { +// let pubsub = PublishSubject() +// let subscriptionTypeMultiple = try GraphQLObjectType( +// name: "Subscription", +// fields: [ +// "importantEmail": GraphQLField (type: EmailEventType), +// "notImportantEmail": GraphQLField (type: EmailEventType) +// ] +// ) +// let testSchema = try GraphQLSchema( +// query: EmailQueryType, +// subscription: subscriptionTypeMultiple +// ) +// let subscription = try createSubscription(pubsub: pubsub, schema: testSchema) +// pubsub.onNext(Email( +// from: "yuzhi@graphql.org", +// subject: "Alright", +// message: "Tests are good", +// unread: true +// )) +// } +// +// +// // TODO Not working. I think it's because it's checking the Resolver return against the Schema-defined return type... +// func testResolverReturningErrorSchema() throws { +// let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) +// let schema = emailSchemaWithResolvers( +// subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in +// return eventLoopGroup.next().makeSucceededFuture(GraphQLError(message: "test error")) +// }, +// resolve: nil +// ) +// let document = try parse(source: """ +// subscription { +// importantEmail +// } +// """) +// let result = try createSourceEventStream( +// queryStrategy: SerialFieldExecutionStrategy(), +// mutationStrategy: SerialFieldExecutionStrategy(), +// subscriptionStrategy: SerialFieldExecutionStrategy(), +// instrumentation: NoOpInstrumentation, +// schema: schema, +// documentAST: document, +// rootValue: Void(), +// context: Void(), +// eventLoopGroup: eventLoopGroup +// ).wait() +// +// switch result { +// case .success: +// XCTFail() +// case .failure(let error): +// let expected = GraphQLError(message:"test error") +// XCTAssertEqual(expected, error) +// } +// } +// +// // Working!!! +// func testResolverThrowingErrorSchema() throws { +// let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) +// let schema = emailSchemaWithResolvers( +// subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in +// throw GraphQLError(message: "test error") +// }, +// resolve: nil +// ) +// let document = try parse(source: """ +// subscription { +// importantEmail +// } +// """) +// let result = try createSourceEventStream( +// queryStrategy: SerialFieldExecutionStrategy(), +// mutationStrategy: SerialFieldExecutionStrategy(), +// subscriptionStrategy: SerialFieldExecutionStrategy(), +// instrumentation: NoOpInstrumentation, +// schema: schema, +// documentAST: document, +// rootValue: Void(), +// context: Void(), +// eventLoopGroup: eventLoopGroup +// ).wait() +// +// switch result { +// case .success(let observable): +// XCTFail() +// case .failure(let error): +// let expected = GraphQLError(message:"test error") +// XCTAssertEqual(expected, error) +// } +// } } let defaultSubscriptionAST = try! parse(source: """ @@ -201,18 +203,18 @@ let defaultSubscriptionAST = try! parse(source: """ """) // MARK: Types -struct Email { +struct Email : Encodable { let from:String let subject:String let message:String let unread:Bool } -struct Inbox { +struct Inbox : Encodable { let emails:[Email] } -struct EmailEvent { +struct EmailEvent : Encodable { let email:Email let inbox:Inbox } @@ -247,13 +249,12 @@ let InboxType = try! GraphQLObjectType( (inbox as! Inbox).emails.count } ), - // TODO figure out how to do searches -// "unread": GraphQLField( -// type: GraphQLInt, -// resolve: { inbox, _, _, _ in -// (inbox as! InboxType).emails. -// } -// ), + "unread": GraphQLField( + type: GraphQLInt, + resolve: { inbox, _, _, _ in + (inbox as! Inbox).emails.filter({$0.unread}).count + } + ), ] ) From 7f3f60a0706d6d77c62608ce61f42f8ae7bd2fb9 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 19 Feb 2021 18:26:39 -0700 Subject: [PATCH 10/36] refactors helpers, adds more tests --- .../Subscription/SubscriptionTests.swift | 379 ++++++++++-------- 1 file changed, 216 insertions(+), 163 deletions(-) diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index aaa353d7..164bcb80 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -6,65 +6,11 @@ import RxSwift class SubscriptionTests : XCTestCase { - /// Creates a subscription result for the input pub/sub, schema, and AST document - private func createSubscription( - pubsub:Observable, - schema:GraphQLSchema? = nil, - document:Document = defaultSubscriptionAST - ) throws -> Observable { - - var emails = [ - Email( - from: "joe@graphql.org", - subject: "Hello", - message: "Hello World", - unread: false - ) - ] - - let testSchema = schema ?? emailSchemaWithResolvers( - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(pubsub) - }, - resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - let email = emailAny as! Email - emails.append(email) - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: emails) - )) - } - ) - - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - let subscriptionResult = try subscribe( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, - schema: testSchema, - documentAST: document, - rootValue: Void(), - context: Void(), - eventLoopGroup: eventLoopGroup, - variableValues: [:], - operationName: nil - ).wait() - - switch subscriptionResult { - case .success(let subscription): - return subscription - case .failure(let error): - throw error - } - } - // MARK: Basic test to see if publishing is working func testBasic() throws { let disposeBag = DisposeBag() let pubsub = PublishSubject() - let subscription = try createSubscription(pubsub: pubsub) + let subscription = try createDbAndSubscription(pubsub: pubsub, query: defaultQuery) let expected = GraphQLResult( data: ["importantEmail": [ @@ -93,115 +39,138 @@ class SubscriptionTests : XCTestCase { // MARK: Subscription Initialization Phase /// accepts multiple subscription fields defined in schema -// func testAcceptsMultipleSubscriptionFields() throws { -// let pubsub = PublishSubject() -// let subscriptionTypeMultiple = try GraphQLObjectType( -// name: "Subscription", -// fields: [ -// "importantEmail": GraphQLField (type: EmailEventType), -// "notImportantEmail": GraphQLField (type: EmailEventType) -// ] -// ) -// let testSchema = try GraphQLSchema( -// query: EmailQueryType, -// subscription: subscriptionTypeMultiple -// ) -// let subscription = try createSubscription(pubsub: pubsub, schema: testSchema) -// pubsub.onNext(Email( -// from: "yuzhi@graphql.org", -// subject: "Alright", -// message: "Tests are good", -// unread: true -// )) -// } -// -// -// // TODO Not working. I think it's because it's checking the Resolver return against the Schema-defined return type... -// func testResolverReturningErrorSchema() throws { -// let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) -// let schema = emailSchemaWithResolvers( -// subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in -// return eventLoopGroup.next().makeSucceededFuture(GraphQLError(message: "test error")) -// }, -// resolve: nil -// ) -// let document = try parse(source: """ -// subscription { -// importantEmail -// } -// """) -// let result = try createSourceEventStream( -// queryStrategy: SerialFieldExecutionStrategy(), -// mutationStrategy: SerialFieldExecutionStrategy(), -// subscriptionStrategy: SerialFieldExecutionStrategy(), -// instrumentation: NoOpInstrumentation, -// schema: schema, -// documentAST: document, -// rootValue: Void(), -// context: Void(), -// eventLoopGroup: eventLoopGroup -// ).wait() -// -// switch result { -// case .success: -// XCTFail() -// case .failure(let error): -// let expected = GraphQLError(message:"test error") -// XCTAssertEqual(expected, error) -// } -// } -// -// // Working!!! -// func testResolverThrowingErrorSchema() throws { -// let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) -// let schema = emailSchemaWithResolvers( -// subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in -// throw GraphQLError(message: "test error") -// }, -// resolve: nil -// ) -// let document = try parse(source: """ -// subscription { -// importantEmail -// } -// """) -// let result = try createSourceEventStream( -// queryStrategy: SerialFieldExecutionStrategy(), -// mutationStrategy: SerialFieldExecutionStrategy(), -// subscriptionStrategy: SerialFieldExecutionStrategy(), -// instrumentation: NoOpInstrumentation, -// schema: schema, -// documentAST: document, -// rootValue: Void(), -// context: Void(), -// eventLoopGroup: eventLoopGroup -// ).wait() -// -// switch result { -// case .success(let observable): -// XCTFail() -// case .failure(let error): -// let expected = GraphQLError(message:"test error") -// XCTAssertEqual(expected, error) -// } -// } + func testAcceptsMultipleSubscriptionFields() throws { + let disposeBag = DisposeBag() + let pubsub = PublishSubject() + + var emails = defaultEmails + let schema = try GraphQLSchema( + query: EmailQueryType, + subscription: try! GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + args: [ + "priority": GraphQLArgument( + type: GraphQLInt + ) + ], + resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + let email = emailAny as! Email + emails.append(email) + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: emails) + )) + }, + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(pubsub) + } + ), + "notImportantEmail": GraphQLField( + type: EmailEventType, + args: [ + "priority": GraphQLArgument( + type: GraphQLInt + ) + ], + resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + let email = emailAny as! Email + emails.append(email) + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: emails) + )) + }, + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(pubsub) + } + ) + ] + ) + ) + let subscription = try createSubscription(pubsub: pubsub, schema: schema, query: defaultQuery) + + let expected = GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 2, + "unread": 1 + ], + "email":[ + "subject": "Alright", + "from": "yuzhi@graphql.org" + ] + ]] + ) + let _ = subscription.subscribe { event in + XCTAssertEqual(event.element, expected) + }.disposed(by: disposeBag) + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + } + + /// 'should only resolve the first field of invalid multi-field' + func testInvalidMultiField() throws { + let disposeBag = DisposeBag() + let pubsub = PublishSubject() + + var didResolveImportantEmail = false + var didResolveNonImportantEmail = false + + let schema = try GraphQLSchema( + query: EmailQueryType, + subscription: try! GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + resolve: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(nil) + }, + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + didResolveImportantEmail = true + return eventLoopGroup.next().makeSucceededFuture(pubsub) + } + ), + "notImportantEmail": GraphQLField( + type: EmailEventType, + resolve: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(nil) + }, + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + didResolveNonImportantEmail = true + return eventLoopGroup.next().makeSucceededFuture(pubsub) + } + ) + ] + ) + ) + let subscription = try createSubscription(pubsub: pubsub, schema: schema, query: """ + subscription { + importantEmail + notImportantEmail + } + """) + + let _ = subscription.subscribe().disposed(by: disposeBag) + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + + XCTAssertTrue(didResolveImportantEmail) + XCTAssertFalse(didResolveNonImportantEmail) + } } -let defaultSubscriptionAST = try! parse(source: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } -""") - // MARK: Types struct Email : Encodable { let from:String @@ -279,7 +248,82 @@ let EmailQueryType = try! GraphQLObjectType( ] ) -func emailSchemaWithResolvers(subscribe: GraphQLFieldResolve?, resolve: GraphQLFieldResolve?) -> GraphQLSchema { +// MARK: Test Helpers + +let defaultQuery = """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } +""" + +let defaultEmails = [ + Email( + from: "joe@graphql.org", + subject: "Hello", + message: "Hello World", + unread: false + ) +] + +/// Generates a default schema and email database, and returns the subscription +private func createDbAndSubscription( + pubsub:Observable, + query:String +) throws -> Observable { + + var emails = defaultEmails + + let schema = emailSchemaWithResolvers( + resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + let email = emailAny as! Email + emails.append(email) + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: emails) + )) + }, + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(pubsub) + } + ) + + return try createSubscription(pubsub: pubsub, schema: schema, query: query) +} + +/// Generates a subscription from the given schema and query. It's expected that the database is managed by the caller. +private func createSubscription( + pubsub: Observable, + schema: GraphQLSchema, + query: String +) throws -> Observable { + let document = try parse(source: query) + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + let subscriptionOrError = try subscribe( + queryStrategy: SerialFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: SerialFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + schema: schema, + documentAST: document, + rootValue: Void(), + context: Void(), + eventLoopGroup: eventLoopGroup, + variableValues: [:], + operationName: nil + ).wait() + return try extractSubscription(subscriptionOrError) +} + +private func emailSchemaWithResolvers(resolve: GraphQLFieldResolve?, subscribe: GraphQLFieldResolve?) -> GraphQLSchema { return try! GraphQLSchema( query: EmailQueryType, subscription: try! GraphQLObjectType( @@ -299,3 +343,12 @@ func emailSchemaWithResolvers(subscribe: GraphQLFieldResolve?, resolve: GraphQLF ) ) } + +private func extractSubscription(_ subscriptionResult: SubscriptionResult) throws -> Observable { + switch subscriptionResult { + case .success(let subscription): + return subscription + case .failure(let error): + throw error + } +} From 8e275091f669e15683a0769bd83a0ef8ad5eb5d9 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 19 Feb 2021 18:27:03 -0700 Subject: [PATCH 11/36] bug fix to extract incorrect names --- Sources/GraphQL/Subscription/Subscribe.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index d33dbaf7..f3615be0 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -199,7 +199,7 @@ func executeSubscription( guard let fieldDef = getFieldDef(schema: context.schema, parentType: type, fieldAST: fieldNode) else { throw GraphQLError.init( - message: "`The subscription field '\(fieldNode.name)' is not defined.`", + message: "`The subscription field '\(fieldNode.name.value)' is not defined.`", nodes: fieldNodes ) } From 07d2409364e807e1796ac7477d0bc3b61ac48104 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 09:43:23 -0700 Subject: [PATCH 12/36] Minor refactor of event resolver for clarity --- Sources/GraphQL/Subscription/Subscribe.swift | 47 ++++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index f3615be0..a52a6e14 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -55,33 +55,33 @@ func subscribe( operationName: operationName, subscribeFieldResolver: subscribeFieldResolver ) - - // For each payload yielded from a subscription, map it over the normal - // GraphQL `execute` function, with `payload` as the rootValue. - // This implements the "MapSourceToResponseEvent" algorithm described in - // the GraphQL specification. The `execute` function provides the - // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the - // "ExecuteQuery" algorithm, for which `execute` is also used. - func mapSourceToResponse(payload: Any) -> EventLoopFuture { - return execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - documentAST: documentAST, - rootValue: payload, // Make payload the root value - context: context, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: operationName - ) - } + return sourceFuture.flatMap{ subscriptionResult -> EventLoopFuture in do { let subscriptionObserver = try subscriptionResult.get() let eventObserver = subscriptionObserver.map { eventPayload -> GraphQLResult in - return try! mapSourceToResponse(payload: eventPayload).wait() // TODO Remove this wait + + // For each payload yielded from a subscription, map it over the normal + // GraphQL `execute` function, with `payload` as the rootValue. + // This implements the "MapSourceToResponseEvent" algorithm described in + // the GraphQL specification. The `execute` function provides the + // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the + // "ExecuteQuery" algorithm, for which `execute` is also used. + let eventResolved = try execute( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + documentAST: documentAST, + rootValue: eventPayload, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ).wait() // TODO remove this wait + + return eventResolved } // TODO Making a future here feels it indicates a mistake... return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.success(eventObserver)) @@ -151,7 +151,6 @@ func createSourceEventStream( eventLoopGroup: eventLoopGroup, rawVariableValues: variableValues, operationName: operationName - // TODO shouldn't we be including the subscribeFieldResolver?? ) return try executeSubscription(context: exeContext, eventLoopGroup: eventLoopGroup) } catch let error as GraphQLError { From 82f3c21fde309a80cb3166e16d9ea85c47386ca5 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 09:55:59 -0700 Subject: [PATCH 13/36] Simplifies MapSourceToResponseEvent futures --- Sources/GraphQL/Subscription/Subscribe.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index a52a6e14..8b70f4fb 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -56,7 +56,7 @@ func subscribe( subscribeFieldResolver: subscribeFieldResolver ) - return sourceFuture.flatMap{ subscriptionResult -> EventLoopFuture in + return sourceFuture.map{ subscriptionResult -> SubscriptionResult in do { let subscriptionObserver = try subscriptionResult.get() let eventObserver = subscriptionObserver.map { eventPayload -> GraphQLResult in @@ -80,13 +80,14 @@ func subscribe( variableValues: variableValues, operationName: operationName ).wait() // TODO remove this wait - return eventResolved } // TODO Making a future here feels it indicates a mistake... - return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.success(eventObserver)) + return SubscriptionResult.success(eventObserver) } catch let graphQLError as GraphQLError { - return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult.failure(graphQLError)) + return SubscriptionResult.failure(graphQLError) + } catch let error { + return SubscriptionResult.failure(GraphQLError(error)) } } } From e789328da3c5748a4add1f1a1a4ffbd32512cba7 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 10:46:37 -0700 Subject: [PATCH 14/36] Changes observer to resolve to a future --- Sources/GraphQL/Subscription/Subscribe.swift | 18 +++++++++--------- .../Subscription/SubscriptionTests.swift | 16 ++++++++++------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 8b70f4fb..5d706a40 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -59,7 +59,7 @@ func subscribe( return sourceFuture.map{ subscriptionResult -> SubscriptionResult in do { let subscriptionObserver = try subscriptionResult.get() - let eventObserver = subscriptionObserver.map { eventPayload -> GraphQLResult in + let eventObserver = subscriptionObserver.map { eventPayload -> Future in // For each payload yielded from a subscription, map it over the normal // GraphQL `execute` function, with `payload` as the rootValue. @@ -67,7 +67,7 @@ func subscribe( // the GraphQL specification. The `execute` function provides the // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the // "ExecuteQuery" algorithm, for which `execute` is also used. - let eventResolved = try execute( + return execute( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -79,10 +79,8 @@ func subscribe( eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName - ).wait() // TODO remove this wait - return eventResolved + ) } - // TODO Making a future here feels it indicates a mistake... return SubscriptionResult.success(eventObserver) } catch let graphQLError as GraphQLError { return SubscriptionResult.failure(graphQLError) @@ -253,7 +251,7 @@ func executeSubscription( return SourceEventStreamResult.failure(context.errors.first!) } else if let error = resolved as? GraphQLError { return SourceEventStreamResult.failure(error) - } else if let observable = resolved as? Observable { + } else if let observable = resolved as? SourceEventStreamObservable { return SourceEventStreamResult.success(observable) } else if resolved == nil { return SourceEventStreamResult.failure( @@ -261,11 +259,13 @@ func executeSubscription( ) } else { return SourceEventStreamResult.failure( - GraphQLError(message: "Subscription field resolver must return an Observable, not \(Swift.type(of:resolved))") + GraphQLError(message: "Subscription field resolver must return an SourceEventStreamObservable, not \(Swift.type(of:resolved))") ) } } } -typealias SubscriptionResult = Result, GraphQLError> -typealias SourceEventStreamResult = Result, GraphQLError> +typealias SubscriptionObservable = Observable> +typealias SubscriptionResult = Result +typealias SourceEventStreamObservable = Observable +typealias SourceEventStreamResult = Result diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index 164bcb80..440f78e2 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -25,7 +25,8 @@ class SubscriptionTests : XCTestCase { ]] ) let _ = subscription.subscribe { event in - XCTAssertEqual(event.element, expected) + let payload = try! event.element!.wait() + XCTAssertEqual(payload, expected) }.disposed(by: disposeBag) pubsub.onNext(Email( from: "yuzhi@graphql.org", @@ -105,7 +106,8 @@ class SubscriptionTests : XCTestCase { ]] ) let _ = subscription.subscribe { event in - XCTAssertEqual(event.element, expected) + let payload = try! event.element!.wait() + XCTAssertEqual(payload, expected) }.disposed(by: disposeBag) pubsub.onNext(Email( from: "yuzhi@graphql.org", @@ -158,7 +160,9 @@ class SubscriptionTests : XCTestCase { } """) - let _ = subscription.subscribe().disposed(by: disposeBag) + let _ = subscription.subscribe{ event in + let _ = try! event.element!.wait() + }.disposed(by: disposeBag) pubsub.onNext(Email( from: "yuzhi@graphql.org", subject: "Alright", @@ -278,7 +282,7 @@ let defaultEmails = [ private func createDbAndSubscription( pubsub:Observable, query:String -) throws -> Observable { +) throws -> SubscriptionObservable { var emails = defaultEmails @@ -304,7 +308,7 @@ private func createSubscription( pubsub: Observable, schema: GraphQLSchema, query: String -) throws -> Observable { +) throws -> SubscriptionObservable { let document = try parse(source: query) let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) let subscriptionOrError = try subscribe( @@ -344,7 +348,7 @@ private func emailSchemaWithResolvers(resolve: GraphQLFieldResolve?, subscribe: ) } -private func extractSubscription(_ subscriptionResult: SubscriptionResult) throws -> Observable { +private func extractSubscription(_ subscriptionResult: SubscriptionResult) throws -> SubscriptionObservable { switch subscriptionResult { case .success(let subscription): return subscription From db299f7f9a550b222c09be73c9c14fb858a43dda Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 15:09:06 -0700 Subject: [PATCH 15/36] Makes location equatable for better error checking --- Sources/GraphQL/Language/Location.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GraphQL/Language/Location.swift b/Sources/GraphQL/Language/Location.swift index a117681c..ab5a49fb 100644 --- a/Sources/GraphQL/Language/Location.swift +++ b/Sources/GraphQL/Language/Location.swift @@ -1,6 +1,6 @@ import Foundation -public struct SourceLocation : Codable { +public struct SourceLocation : Codable, Equatable { public let line: Int public let column: Int From f6b5eaeb590e74ab50b10f0afd1213d90a9264cc Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 15:09:55 -0700 Subject: [PATCH 16/36] Improves error message for incorrect arg types --- Sources/GraphQL/Utilities/IsValidValue.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/GraphQL/Utilities/IsValidValue.swift b/Sources/GraphQL/Utilities/IsValidValue.swift index 28513db3..6edec555 100644 --- a/Sources/GraphQL/Utilities/IsValidValue.swift +++ b/Sources/GraphQL/Utilities/IsValidValue.swift @@ -75,9 +75,12 @@ func isValidValue(value: Map, type: GraphQLInputType) throws -> [String] { // Scalar/Enum input checks to ensure the type can parse the value to // a non-null value. - let parseResult = try type.parseValue(value: value) - - if parseResult == .null { + do { + let parseResult = try type.parseValue(value: value) + if parseResult == .null { + return ["Expected type \"\(type.name)\", found \(value)."] + } + } catch { return ["Expected type \"\(type.name)\", found \(value)."] } From f020990693724299ad2d7d62520dfddc353e8136 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 15:11:29 -0700 Subject: [PATCH 17/36] Improves resolver return type issue message --- Sources/GraphQL/Subscription/Subscribe.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 5d706a40..01a070ce 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -258,8 +258,11 @@ func executeSubscription( GraphQLError(message: "Resolved subscription was nil") ) } else { + let resolvedObj = resolved as AnyObject return SourceEventStreamResult.failure( - GraphQLError(message: "Subscription field resolver must return an SourceEventStreamObservable, not \(Swift.type(of:resolved))") + GraphQLError( + message: "Subscription field resolver must return SourceEventStreamObservable. Received: '\(resolvedObj)'" + ) ) } } From aa9d7f8122899b4f0ecdbe0020300e207404dc5c Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 15:11:52 -0700 Subject: [PATCH 18/36] Adds remaining graphql-js subscription tests --- .../Subscription/SubscriptionTests.swift | 503 ++++++++++++++++-- 1 file changed, 449 insertions(+), 54 deletions(-) diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index 440f78e2..74d41c4a 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -3,39 +3,8 @@ import NIO import RxSwift @testable import GraphQL - +/// This follows the graphql-js testing, with deviations where noted. class SubscriptionTests : XCTestCase { - - // MARK: Basic test to see if publishing is working - func testBasic() throws { - let disposeBag = DisposeBag() - let pubsub = PublishSubject() - let subscription = try createDbAndSubscription(pubsub: pubsub, query: defaultQuery) - - let expected = GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 2, - "unread": 1 - ], - "email":[ - "subject": "Alright", - "from": "yuzhi@graphql.org" - ] - ]] - ) - let _ = subscription.subscribe { event in - let payload = try! event.element!.wait() - XCTAssertEqual(payload, expected) - }.disposed(by: disposeBag) - pubsub.onNext(Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - } - // MARK: Subscription Initialization Phase @@ -43,7 +12,7 @@ class SubscriptionTests : XCTestCase { func testAcceptsMultipleSubscriptionFields() throws { let disposeBag = DisposeBag() let pubsub = PublishSubject() - + var emails = defaultEmails let schema = try GraphQLSchema( query: EmailQueryType, @@ -91,8 +60,8 @@ class SubscriptionTests : XCTestCase { ] ) ) - let subscription = try createSubscription(pubsub: pubsub, schema: schema, query: defaultQuery) - + let subscription = try createSubscription(pubsub: pubsub, schema: schema, query: nestedQuery) + let expected = GraphQLResult( data: ["importantEmail": [ "inbox":[ @@ -116,15 +85,17 @@ class SubscriptionTests : XCTestCase { unread: true )) } - + /// 'should only resolve the first field of invalid multi-field' + /// + /// Note that due to implementation details in Swift, this will not resolve the "first" one, but rather a random one of the two func testInvalidMultiField() throws { let disposeBag = DisposeBag() let pubsub = PublishSubject() - + var didResolveImportantEmail = false var didResolveNonImportantEmail = false - + let schema = try GraphQLSchema( query: EmailQueryType, subscription: try! GraphQLObjectType( @@ -159,7 +130,7 @@ class SubscriptionTests : XCTestCase { notImportantEmail } """) - + let _ = subscription.subscribe{ event in let _ = try! event.element!.wait() }.disposed(by: disposeBag) @@ -169,10 +140,423 @@ class SubscriptionTests : XCTestCase { message: "Tests are good", unread: true )) + + // One, and only one should be true + XCTAssertTrue(didResolveImportantEmail || didResolveNonImportantEmail) + XCTAssertFalse(didResolveImportantEmail && didResolveNonImportantEmail) + } + + // 'throws an error if schema is missing' + // Not implemented because this is taken care of by Swift optional types + + // 'throws an error if document is missing' + // Not implemented because this is taken care of by Swift optional types + + /// 'resolves to an error for unknown subscription field' + func testErrorUnknownSubscriptionField() throws { + let pubsub = PublishSubject() + XCTAssertThrowsError( + try createDbAndSubscription(pubsub: pubsub, query: """ + subscription { + unknownField + } + """ + ) + ) { error in + let graphQlError = error as! GraphQLError + XCTAssertEqual(graphQlError.message, "`The subscription field 'unknownField' is not defined.`") + XCTAssertEqual(graphQlError.locations, [SourceLocation(line: 2, column: 5)]) + } + } + + /// 'should pass through unexpected errors thrown in subscribe' + func testPassUnexpectedSubscribeErrors() throws { + let pubsub = PublishSubject() + XCTAssertThrowsError( + try createDbAndSubscription(pubsub: pubsub, query: "") + ) + } + + /// 'throws an error if subscribe does not return an iterator' + func testErrorIfSubscribeIsntIterator() throws { + let pubsub = PublishSubject() + let schema = try GraphQLSchema( + query: EmailQueryType, + subscription: try! GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + resolve: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(nil) + }, + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture("test") + } + ) + ] + ) + ) + XCTAssertThrowsError( + try createSubscription(pubsub: pubsub, schema: schema, query: basicQuery) + ) { error in + let graphQlError = error as! GraphQLError + XCTAssertEqual( + graphQlError.message, + "Subscription field resolver must return SourceEventStreamObservable. Received: 'test'" + ) + } + } + + /// 'resolves to an error for subscription resolver errors' + func testErrorForSubscriptionResolverErrors() throws { + + let pubsub = PublishSubject() + func verifyError(schema: GraphQLSchema) { + XCTAssertThrowsError( + try createSubscription(pubsub: pubsub, schema: schema, query: basicQuery) + ) { error in + let graphQlError = error as! GraphQLError + XCTAssertEqual(graphQlError.message, "test error") + } + } + + // Throwing an error + verifyError(schema: emailSchemaWithResolvers( + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + throw GraphQLError(message: "test error") + } + )) + + // Resolving to an error + verifyError(schema: emailSchemaWithResolvers( + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(GraphQLError(message: "test error")) + } + )) + + // Rejecting with an error + verifyError(schema: emailSchemaWithResolvers( + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeFailedFuture(GraphQLError(message: "test error")) + } + )) + } + + + /// 'resolves to an error for source event stream resolver errors' + // Tests above cover this + + /// 'resolves to an error if variables were wrong type' + func testErrorVariablesWrongType() throws { + let pubsub = PublishSubject() + + let query = """ + subscription ($priority: Int) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """ + + XCTAssertThrowsError( + try createDbAndSubscription( + pubsub: pubsub, + query: query, + variableValues: [ + "priority": "meow" + ] + ) + ) { error in + let graphQlError = error as! GraphQLError + XCTAssertEqual( + graphQlError.message, + "Variable \"$priority\" got invalid value \"meow\".\nExpected type \"Int\", found \"meow\"." + ) + } + } + + + // MARK: Subscription Publish Phase + + /// 'produces a payload for a single subscriber' + func testSingleSubscriber() throws { + let disposeBag = DisposeBag() + let pubsub = PublishSubject() + let subscription = try createDbAndSubscription(pubsub: pubsub, query: nestedQuery) + + let expected = GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 2, + "unread": 1 + ], + "email":[ + "subject": "Alright", + "from": "yuzhi@graphql.org" + ] + ]] + ) + let _ = subscription.subscribe { event in + let payload = try! event.element!.wait() + XCTAssertEqual(payload, expected) + }.disposed(by: disposeBag) + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + } + + /// 'produces a payload for multiple subscribe in same subscription' + func testMultipleSubscribers() throws { + let disposeBag = DisposeBag() + let pubsub = PublishSubject() + let subscription = try createDbAndSubscription(pubsub: pubsub, query: nestedQuery) + + let expected = GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 2, + "unread": 1 + ], + "email":[ + "subject": "Alright", + "from": "yuzhi@graphql.org" + ] + ]] + ) + // Subscription 1 + let _ = subscription.subscribe { event in + XCTAssertEqual(try! event.element!.wait(), expected) + }.disposed(by: disposeBag) + + // TODO fix our pub/sub implementation so that we don't append an email for every subscriber (instead only on every distinct event) + + // Subscription 2 + let _ = subscription.subscribe { event in + XCTAssertEqual(try! event.element!.wait(), expected) + }.disposed(by: disposeBag) + + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + } + + /// 'produces a payload per subscription event' + func testPayloadPerEvent() throws { + let disposeBag = DisposeBag() + let pubsub = PublishSubject() + let subscription = try createDbAndSubscription(pubsub: pubsub, query: nestedQuery) + + let expected = [ + GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 2, + "unread": 1 + ], + "email":[ + "subject": "Alright", + "from": "yuzhi@graphql.org" + ] + ]] + ), + GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 3, + "unread": 2 + ], + "email":[ + "subject": "Tools", + "from": "hyo@graphql.org" + ] + ]] + ), + ] + + var eventCounter = 0 + let _ = subscription.subscribe { event in + XCTAssertEqual(try! event.element!.wait(), expected[eventCounter]) + eventCounter += 1 + }.disposed(by: disposeBag) + + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + pubsub.onNext(Email( + from: "hyo@graphql.org", + subject: "Tools", + message: "I <3 making things", + unread: true + )) - XCTAssertTrue(didResolveImportantEmail) - XCTAssertFalse(didResolveNonImportantEmail) + XCTAssertEqual(eventCounter, 2) } + + /// 'should not trigger when subscription is already done' + func testNoTriggerAfterDone() throws { + let pubsub = PublishSubject() + let subscription = try createDbAndSubscription(pubsub: pubsub, query: nestedQuery) + + let expected = GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 2, + "unread": 1 + ], + "email":[ + "subject": "Alright", + "from": "yuzhi@graphql.org" + ] + ]] + ) + + var eventCounter = 0 + let subscriber = subscription.subscribe { event in + XCTAssertEqual(try! event.element!.wait(), expected) + eventCounter += 1 + } + + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + + subscriber.dispose() + + // This should not trigger an event. + pubsub.onNext(Email( + from: "hyo@graphql.org", + subject: "Tools", + message: "I <3 making things", + unread: true + )) + XCTAssertEqual(eventCounter, 1) + } + + /// 'should not trigger when subscription is thrown' + // Not necessary - Pub/sub implementation handles throwing/closing itself. + + /// 'event order is correct for multiple publishes' + // Not necessary - Pub/sub implementation handles event ordering + + /// 'should handle error during execution of source event' + func testErrorDuringSubscription() throws { + let disposeBag = DisposeBag() + let pubsub = PublishSubject() + + var emails = defaultEmails + let observer = pubsub.do( + onNext: { emailAny in + let email = emailAny as! Email + emails.append(email) + } + ) + + let schema = emailSchemaWithResolvers( + resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + let email = emailAny as! Email + if email.subject == "Goodbye" { // Force the system to fail here. + throw GraphQLError(message:"Never leave.") + } + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: emails) + )) + }, + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(observer) + } + ) + + let subscription = try createSubscription(pubsub: pubsub, schema: schema, query: """ + subscription { + importantEmail { + email { + subject + } + } + } + """) + + let expected = [ + GraphQLResult( + data: ["importantEmail": [ + "email":[ + "subject": "Hello" + ] + ]] + ), + GraphQLResult( // An error in execution is presented as such. + data: ["importantEmail": nil], + errors: [ + GraphQLError(message: "Never leave.") + ] + ), + GraphQLResult( // However that does not close the response event stream. Subsequent events are still executed. + data: ["importantEmail": [ + "email":[ + "subject": "Bonjour" + ] + ]] + ) + ] + + var eventCounter = 0 + let _ = subscription.subscribe { event in + XCTAssertEqual(try! event.element!.wait(), expected[eventCounter]) + eventCounter += 1 + }.disposed(by: disposeBag) + + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Hello", + message: "Tests are good", + unread: true + )) + + // An error in execution is presented as such. + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Goodbye", + message: "Tests are good", + unread: true + )) + + // However that does not close the response event stream. Subsequent events are still executed. + pubsub.onNext(Email( + from: "yuzhi@graphql.org", + subject: "Bonjour", + message: "Tests are good", + unread: true + )) + + XCTAssertEqual(eventCounter, 3) + } + + /// 'should pass through error thrown in source event stream' + // Not necessary - Pub/sub implementation handles event erroring + + /// 'should resolve GraphQL error from source event stream' + // Not necessary - Pub/sub implementation handles event erroring } // MARK: Types @@ -253,8 +637,14 @@ let EmailQueryType = try! GraphQLObjectType( ) // MARK: Test Helpers +let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) -let defaultQuery = """ +let basicQuery = """ + subscription { + importantEmail + } +""" +let nestedQuery = """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -280,37 +670,42 @@ let defaultEmails = [ /// Generates a default schema and email database, and returns the subscription private func createDbAndSubscription( - pubsub:Observable, - query:String + pubsub:PublishSubject, + query:String, + variableValues: [String: Map] = [:] ) throws -> SubscriptionObservable { - var emails = defaultEmails + let observer = pubsub.do( + onNext: { emailAny in + let email = emailAny as! Email + emails.append(email) + } + ) let schema = emailSchemaWithResolvers( resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in let email = emailAny as! Email - emails.append(email) return eventLoopGroup.next().makeSucceededFuture(EmailEvent( email: email, inbox: Inbox(emails: emails) )) }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(pubsub) + return eventLoopGroup.next().makeSucceededFuture(observer) } ) - return try createSubscription(pubsub: pubsub, schema: schema, query: query) + return try createSubscription(pubsub: pubsub, schema: schema, query: query, variableValues: variableValues) } /// Generates a subscription from the given schema and query. It's expected that the database is managed by the caller. private func createSubscription( pubsub: Observable, schema: GraphQLSchema, - query: String + query: String, + variableValues: [String: Map] = [:] ) throws -> SubscriptionObservable { let document = try parse(source: query) - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) let subscriptionOrError = try subscribe( queryStrategy: SerialFieldExecutionStrategy(), mutationStrategy: SerialFieldExecutionStrategy(), @@ -321,13 +716,13 @@ private func createSubscription( rootValue: Void(), context: Void(), eventLoopGroup: eventLoopGroup, - variableValues: [:], + variableValues: variableValues, operationName: nil ).wait() - return try extractSubscription(subscriptionOrError) + return try extractSubscriptionFromResult(subscriptionOrError) } -private func emailSchemaWithResolvers(resolve: GraphQLFieldResolve?, subscribe: GraphQLFieldResolve?) -> GraphQLSchema { +private func emailSchemaWithResolvers(resolve: GraphQLFieldResolve? = nil, subscribe: GraphQLFieldResolve? = nil) -> GraphQLSchema { return try! GraphQLSchema( query: EmailQueryType, subscription: try! GraphQLObjectType( @@ -348,7 +743,7 @@ private func emailSchemaWithResolvers(resolve: GraphQLFieldResolve?, subscribe: ) } -private func extractSubscription(_ subscriptionResult: SubscriptionResult) throws -> SubscriptionObservable { +private func extractSubscriptionFromResult(_ subscriptionResult: SubscriptionResult) throws -> SubscriptionObservable { switch subscriptionResult { case .success(let subscription): return subscription From bff1618d21e9b26f54e0e8a775afa080ea2eb6cc Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 16:02:46 -0700 Subject: [PATCH 19/36] Refactored to support multiple subscribers --- .../Subscription/SubscriptionTests.swift | 197 +++++++++--------- 1 file changed, 95 insertions(+), 102 deletions(-) diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index 74d41c4a..a008c406 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -11,9 +11,7 @@ class SubscriptionTests : XCTestCase { /// accepts multiple subscription fields defined in schema func testAcceptsMultipleSubscriptionFields() throws { let disposeBag = DisposeBag() - let pubsub = PublishSubject() - - var emails = defaultEmails + let db = EmailDb() let schema = try GraphQLSchema( query: EmailQueryType, subscription: try! GraphQLObjectType( @@ -28,14 +26,13 @@ class SubscriptionTests : XCTestCase { ], resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in let email = emailAny as! Email - emails.append(email) return eventLoopGroup.next().makeSucceededFuture(EmailEvent( email: email, - inbox: Inbox(emails: emails) + inbox: Inbox(emails: db.emails) )) }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(pubsub) + return eventLoopGroup.next().makeSucceededFuture(db.publisher) } ), "notImportantEmail": GraphQLField( @@ -47,20 +44,19 @@ class SubscriptionTests : XCTestCase { ], resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in let email = emailAny as! Email - emails.append(email) return eventLoopGroup.next().makeSucceededFuture(EmailEvent( email: email, - inbox: Inbox(emails: emails) + inbox: Inbox(emails: db.emails) )) }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(pubsub) + return eventLoopGroup.next().makeSucceededFuture(db.publisher) } ) ] ) ) - let subscription = try createSubscription(pubsub: pubsub, schema: schema, query: nestedQuery) + let subscription = try createSubscription(pubsub: db.publisher, schema: schema, query: nestedQuery) let expected = GraphQLResult( data: ["importantEmail": [ @@ -78,7 +74,7 @@ class SubscriptionTests : XCTestCase { let payload = try! event.element!.wait() XCTAssertEqual(payload, expected) }.disposed(by: disposeBag) - pubsub.onNext(Email( + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", @@ -91,7 +87,7 @@ class SubscriptionTests : XCTestCase { /// Note that due to implementation details in Swift, this will not resolve the "first" one, but rather a random one of the two func testInvalidMultiField() throws { let disposeBag = DisposeBag() - let pubsub = PublishSubject() + let db = EmailDb() var didResolveImportantEmail = false var didResolveNonImportantEmail = false @@ -108,7 +104,7 @@ class SubscriptionTests : XCTestCase { }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in didResolveImportantEmail = true - return eventLoopGroup.next().makeSucceededFuture(pubsub) + return eventLoopGroup.next().makeSucceededFuture(db.publisher) } ), "notImportantEmail": GraphQLField( @@ -118,13 +114,13 @@ class SubscriptionTests : XCTestCase { }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in didResolveNonImportantEmail = true - return eventLoopGroup.next().makeSucceededFuture(pubsub) + return eventLoopGroup.next().makeSucceededFuture(db.publisher) } ) ] ) ) - let subscription = try createSubscription(pubsub: pubsub, schema: schema, query: """ + let subscription = try createSubscription(pubsub: db.publisher, schema: schema, query: """ subscription { importantEmail notImportantEmail @@ -134,7 +130,7 @@ class SubscriptionTests : XCTestCase { let _ = subscription.subscribe{ event in let _ = try! event.element!.wait() }.disposed(by: disposeBag) - pubsub.onNext(Email( + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", @@ -154,9 +150,9 @@ class SubscriptionTests : XCTestCase { /// 'resolves to an error for unknown subscription field' func testErrorUnknownSubscriptionField() throws { - let pubsub = PublishSubject() + let db = EmailDb() XCTAssertThrowsError( - try createDbAndSubscription(pubsub: pubsub, query: """ + try defaultSubscription(db: db, query: """ subscription { unknownField } @@ -171,15 +167,15 @@ class SubscriptionTests : XCTestCase { /// 'should pass through unexpected errors thrown in subscribe' func testPassUnexpectedSubscribeErrors() throws { - let pubsub = PublishSubject() + let db = EmailDb() XCTAssertThrowsError( - try createDbAndSubscription(pubsub: pubsub, query: "") + try defaultSubscription(db: db, query: "") ) } /// 'throws an error if subscribe does not return an iterator' func testErrorIfSubscribeIsntIterator() throws { - let pubsub = PublishSubject() + let db = EmailDb() let schema = try GraphQLSchema( query: EmailQueryType, subscription: try! GraphQLObjectType( @@ -198,7 +194,7 @@ class SubscriptionTests : XCTestCase { ) ) XCTAssertThrowsError( - try createSubscription(pubsub: pubsub, schema: schema, query: basicQuery) + try createSubscription(pubsub: db.publisher, schema: schema, query: basicQuery) ) { error in let graphQlError = error as! GraphQLError XCTAssertEqual( @@ -210,11 +206,10 @@ class SubscriptionTests : XCTestCase { /// 'resolves to an error for subscription resolver errors' func testErrorForSubscriptionResolverErrors() throws { - - let pubsub = PublishSubject() + let db = EmailDb() func verifyError(schema: GraphQLSchema) { XCTAssertThrowsError( - try createSubscription(pubsub: pubsub, schema: schema, query: basicQuery) + try createSubscription(pubsub: db.publisher, schema: schema, query: basicQuery) ) { error in let graphQlError = error as! GraphQLError XCTAssertEqual(graphQlError.message, "test error") @@ -249,8 +244,7 @@ class SubscriptionTests : XCTestCase { /// 'resolves to an error if variables were wrong type' func testErrorVariablesWrongType() throws { - let pubsub = PublishSubject() - + let db = EmailDb() let query = """ subscription ($priority: Int) { importantEmail(priority: $priority) { @@ -267,8 +261,8 @@ class SubscriptionTests : XCTestCase { """ XCTAssertThrowsError( - try createDbAndSubscription( - pubsub: pubsub, + try defaultSubscription( + db: db, query: query, variableValues: [ "priority": "meow" @@ -285,13 +279,13 @@ class SubscriptionTests : XCTestCase { // MARK: Subscription Publish Phase - + /// 'produces a payload for a single subscriber' func testSingleSubscriber() throws { let disposeBag = DisposeBag() - let pubsub = PublishSubject() - let subscription = try createDbAndSubscription(pubsub: pubsub, query: nestedQuery) - + let db = EmailDb() + let subscription = try defaultSubscription(db: db, query: nestedQuery) + let expected = GraphQLResult( data: ["importantEmail": [ "inbox":[ @@ -308,7 +302,7 @@ class SubscriptionTests : XCTestCase { let payload = try! event.element!.wait() XCTAssertEqual(payload, expected) }.disposed(by: disposeBag) - pubsub.onNext(Email( + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", @@ -319,8 +313,8 @@ class SubscriptionTests : XCTestCase { /// 'produces a payload for multiple subscribe in same subscription' func testMultipleSubscribers() throws { let disposeBag = DisposeBag() - let pubsub = PublishSubject() - let subscription = try createDbAndSubscription(pubsub: pubsub, query: nestedQuery) + let db = EmailDb() + let subscription = try defaultSubscription(db: db, query: nestedQuery) let expected = GraphQLResult( data: ["importantEmail": [ @@ -339,14 +333,12 @@ class SubscriptionTests : XCTestCase { XCTAssertEqual(try! event.element!.wait(), expected) }.disposed(by: disposeBag) - // TODO fix our pub/sub implementation so that we don't append an email for every subscriber (instead only on every distinct event) - // Subscription 2 let _ = subscription.subscribe { event in XCTAssertEqual(try! event.element!.wait(), expected) }.disposed(by: disposeBag) - pubsub.onNext(Email( + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", @@ -357,8 +349,8 @@ class SubscriptionTests : XCTestCase { /// 'produces a payload per subscription event' func testPayloadPerEvent() throws { let disposeBag = DisposeBag() - let pubsub = PublishSubject() - let subscription = try createDbAndSubscription(pubsub: pubsub, query: nestedQuery) + let db = EmailDb() + let subscription = try defaultSubscription(db: db, query: nestedQuery) let expected = [ GraphQLResult( @@ -386,33 +378,33 @@ class SubscriptionTests : XCTestCase { ]] ), ] - + var eventCounter = 0 let _ = subscription.subscribe { event in XCTAssertEqual(try! event.element!.wait(), expected[eventCounter]) eventCounter += 1 }.disposed(by: disposeBag) - - pubsub.onNext(Email( + + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true )) - pubsub.onNext(Email( + db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", unread: true )) - + XCTAssertEqual(eventCounter, 2) } - + /// 'should not trigger when subscription is already done' func testNoTriggerAfterDone() throws { - let pubsub = PublishSubject() - let subscription = try createDbAndSubscription(pubsub: pubsub, query: nestedQuery) + let db = EmailDb() + let subscription = try defaultSubscription(db: db, query: nestedQuery) let expected = GraphQLResult( data: ["importantEmail": [ @@ -426,24 +418,24 @@ class SubscriptionTests : XCTestCase { ] ]] ) - + var eventCounter = 0 let subscriber = subscription.subscribe { event in XCTAssertEqual(try! event.element!.wait(), expected) eventCounter += 1 } - - pubsub.onNext(Email( + + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true )) - + subscriber.dispose() - + // This should not trigger an event. - pubsub.onNext(Email( + db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", @@ -451,26 +443,18 @@ class SubscriptionTests : XCTestCase { )) XCTAssertEqual(eventCounter, 1) } - + /// 'should not trigger when subscription is thrown' // Not necessary - Pub/sub implementation handles throwing/closing itself. - + /// 'event order is correct for multiple publishes' // Not necessary - Pub/sub implementation handles event ordering - + /// 'should handle error during execution of source event' func testErrorDuringSubscription() throws { let disposeBag = DisposeBag() - let pubsub = PublishSubject() - - var emails = defaultEmails - let observer = pubsub.do( - onNext: { emailAny in - let email = emailAny as! Email - emails.append(email) - } - ) - + let db = EmailDb() + let schema = emailSchemaWithResolvers( resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in let email = emailAny as! Email @@ -479,15 +463,15 @@ class SubscriptionTests : XCTestCase { } return eventLoopGroup.next().makeSucceededFuture(EmailEvent( email: email, - inbox: Inbox(emails: emails) + inbox: Inbox(emails: db.emails) )) }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(observer) + return eventLoopGroup.next().makeSucceededFuture(db.publisher) } ) - - let subscription = try createSubscription(pubsub: pubsub, schema: schema, query: """ + + let subscription = try createSubscription(pubsub: db.publisher, schema: schema, query: """ subscription { importantEmail { email { @@ -496,7 +480,7 @@ class SubscriptionTests : XCTestCase { } } """) - + let expected = [ GraphQLResult( data: ["importantEmail": [ @@ -519,36 +503,36 @@ class SubscriptionTests : XCTestCase { ]] ) ] - + var eventCounter = 0 let _ = subscription.subscribe { event in XCTAssertEqual(try! event.element!.wait(), expected[eventCounter]) eventCounter += 1 }.disposed(by: disposeBag) - - pubsub.onNext(Email( + + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Hello", message: "Tests are good", unread: true )) - + // An error in execution is presented as such. - pubsub.onNext(Email( + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Goodbye", message: "Tests are good", unread: true )) - + // However that does not close the response event stream. Subsequent events are still executed. - pubsub.onNext(Email( + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Bonjour", message: "Tests are good", unread: true )) - + XCTAssertEqual(eventCounter, 3) } @@ -637,6 +621,9 @@ let EmailQueryType = try! GraphQLObjectType( ) // MARK: Test Helpers + +// TODO: I seem to be getting some thread deadlocking when I set this to system count. FIX +//let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) let basicQuery = """ @@ -659,43 +646,49 @@ let nestedQuery = """ } """ -let defaultEmails = [ - Email( - from: "joe@graphql.org", - subject: "Hello", - message: "Hello World", - unread: false - ) -] +class EmailDb { + var emails: [Email] + let publisher: PublishSubject + + init() { + emails = [ + Email( + from: "joe@graphql.org", + subject: "Hello", + message: "Hello World", + unread: false + ) + ] + publisher = PublishSubject() + } + + func trigger(email:Email) { + emails.append(email) + publisher.onNext(email) + } +} -/// Generates a default schema and email database, and returns the subscription -private func createDbAndSubscription( - pubsub:PublishSubject, +/// Generates a default schema and resolvers, and returns the subscription +private func defaultSubscription( + db:EmailDb, query:String, variableValues: [String: Map] = [:] ) throws -> SubscriptionObservable { - var emails = defaultEmails - let observer = pubsub.do( - onNext: { emailAny in - let email = emailAny as! Email - emails.append(email) - } - ) let schema = emailSchemaWithResolvers( resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in let email = emailAny as! Email return eventLoopGroup.next().makeSucceededFuture(EmailEvent( email: email, - inbox: Inbox(emails: emails) + inbox: Inbox(emails: db.emails) )) }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(observer) + return eventLoopGroup.next().makeSucceededFuture(db.publisher) } ) - return try createSubscription(pubsub: pubsub, schema: schema, query: query, variableValues: variableValues) + return try createSubscription(pubsub: db.publisher, schema: schema, query: query, variableValues: variableValues) } /// Generates a subscription from the given schema and query. It's expected that the database is managed by the caller. From 8040a8703802aede713158cf8e5932ae7f7206fb Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 16:46:58 -0700 Subject: [PATCH 20/36] Removes unused parameters --- Sources/GraphQL/Subscription/Subscribe.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 01a070ce..c1cd2245 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -35,9 +35,7 @@ func subscribe( context: Any, eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], - operationName: String? = nil, - fieldResolver: GraphQLFieldResolve? = nil, - subscribeFieldResolver: GraphQLFieldResolve? = nil + operationName: String? = nil ) -> EventLoopFuture { @@ -52,8 +50,7 @@ func subscribe( context: context, eventLoopGroup: eventLoopGroup, variableValues: variableValues, - operationName: operationName, - subscribeFieldResolver: subscribeFieldResolver + operationName: operationName ) return sourceFuture.map{ subscriptionResult -> SubscriptionResult in @@ -129,8 +126,7 @@ func createSourceEventStream( context: Any, eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], - operationName: String? = nil, - subscribeFieldResolver: GraphQLFieldResolve? = nil + operationName: String? = nil ) -> EventLoopFuture { let executeStarted = instrumentation.now From f6b92071e9068e056d227dd58610095545337eb3 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 16:48:14 -0700 Subject: [PATCH 21/36] Refactors tests to be simpler and more logical --- .../Subscription/SubscriptionTests.swift | 473 ++++++++++-------- 1 file changed, 251 insertions(+), 222 deletions(-) diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index a008c406..c5d4d21a 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -10,7 +10,6 @@ class SubscriptionTests : XCTestCase { /// accepts multiple subscription fields defined in schema func testAcceptsMultipleSubscriptionFields() throws { - let disposeBag = DisposeBag() let db = EmailDb() let schema = try GraphQLSchema( query: EmailQueryType, @@ -56,9 +55,34 @@ class SubscriptionTests : XCTestCase { ] ) ) - let subscription = try createSubscription(pubsub: db.publisher, schema: schema, query: nestedQuery) - - let expected = GraphQLResult( + let subscription = try createSubscription(schema: schema, query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + + var currentResult = GraphQLResult() + let _ = subscription.subscribe { event in + currentResult = try! event.element!.wait() + }.disposed(by: db.disposeBag) + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + + XCTAssertEqual(currentResult, GraphQLResult( data: ["importantEmail": [ "inbox":[ "total": 2, @@ -69,16 +93,6 @@ class SubscriptionTests : XCTestCase { "from": "yuzhi@graphql.org" ] ]] - ) - let _ = subscription.subscribe { event in - let payload = try! event.element!.wait() - XCTAssertEqual(payload, expected) - }.disposed(by: disposeBag) - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true )) } @@ -86,7 +100,6 @@ class SubscriptionTests : XCTestCase { /// /// Note that due to implementation details in Swift, this will not resolve the "first" one, but rather a random one of the two func testInvalidMultiField() throws { - let disposeBag = DisposeBag() let db = EmailDb() var didResolveImportantEmail = false @@ -120,7 +133,7 @@ class SubscriptionTests : XCTestCase { ] ) ) - let subscription = try createSubscription(pubsub: db.publisher, schema: schema, query: """ + let subscription = try createSubscription(schema: schema, query: """ subscription { importantEmail notImportantEmail @@ -129,7 +142,7 @@ class SubscriptionTests : XCTestCase { let _ = subscription.subscribe{ event in let _ = try! event.element!.wait() - }.disposed(by: disposeBag) + }.disposed(by: db.disposeBag) db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", @@ -137,7 +150,7 @@ class SubscriptionTests : XCTestCase { unread: true )) - // One, and only one should be true + // One and only one should be true XCTAssertTrue(didResolveImportantEmail || didResolveNonImportantEmail) XCTAssertFalse(didResolveImportantEmail && didResolveNonImportantEmail) } @@ -152,7 +165,7 @@ class SubscriptionTests : XCTestCase { func testErrorUnknownSubscriptionField() throws { let db = EmailDb() XCTAssertThrowsError( - try defaultSubscription(db: db, query: """ + try db.subscription(query: """ subscription { unknownField } @@ -169,32 +182,26 @@ class SubscriptionTests : XCTestCase { func testPassUnexpectedSubscribeErrors() throws { let db = EmailDb() XCTAssertThrowsError( - try defaultSubscription(db: db, query: "") + try db.subscription(query: "") ) } /// 'throws an error if subscribe does not return an iterator' func testErrorIfSubscribeIsntIterator() throws { - let db = EmailDb() - let schema = try GraphQLSchema( - query: EmailQueryType, - subscription: try! GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - resolve: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(nil) - }, - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture("test") - } - ) - ] - ) + let schema = emailSchemaWithResolvers( + resolve: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(nil) + }, + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture("test") + } ) XCTAssertThrowsError( - try createSubscription(pubsub: db.publisher, schema: schema, query: basicQuery) + try createSubscription(schema: schema, query: """ + subscription { + importantEmail + } + """) ) { error in let graphQlError = error as! GraphQLError XCTAssertEqual( @@ -206,10 +213,13 @@ class SubscriptionTests : XCTestCase { /// 'resolves to an error for subscription resolver errors' func testErrorForSubscriptionResolverErrors() throws { - let db = EmailDb() func verifyError(schema: GraphQLSchema) { XCTAssertThrowsError( - try createSubscription(pubsub: db.publisher, schema: schema, query: basicQuery) + try createSubscription(schema: schema, query: """ + subscription { + importantEmail + } + """) ) { error in let graphQlError = error as! GraphQLError XCTAssertEqual(graphQlError.message, "test error") @@ -261,8 +271,7 @@ class SubscriptionTests : XCTestCase { """ XCTAssertThrowsError( - try defaultSubscription( - db: db, + try db.subscription( query: query, variableValues: [ "priority": "meow" @@ -282,11 +291,34 @@ class SubscriptionTests : XCTestCase { /// 'produces a payload for a single subscriber' func testSingleSubscriber() throws { - let disposeBag = DisposeBag() let db = EmailDb() - let subscription = try defaultSubscription(db: db, query: nestedQuery) - - let expected = GraphQLResult( + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + + var currentResult = GraphQLResult() + let _ = subscription.subscribe { event in + currentResult = try! event.element!.wait() + }.disposed(by: db.disposeBag) + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + XCTAssertEqual(currentResult, GraphQLResult( data: ["importantEmail": [ "inbox":[ "total": 2, @@ -297,25 +329,46 @@ class SubscriptionTests : XCTestCase { "from": "yuzhi@graphql.org" ] ]] - ) - let _ = subscription.subscribe { event in - let payload = try! event.element!.wait() - XCTAssertEqual(payload, expected) - }.disposed(by: disposeBag) - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true )) } /// 'produces a payload for multiple subscribe in same subscription' func testMultipleSubscribers() throws { - let disposeBag = DisposeBag() let db = EmailDb() - let subscription = try defaultSubscription(db: db, query: nestedQuery) + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + + // Subscription 1 + var sub1Value = GraphQLResult() + let _ = subscription.subscribe { event in + sub1Value = try! event.element!.wait() + }.disposed(by: db.disposeBag) + + // Subscription 2 + var sub2Value = GraphQLResult() + let _ = subscription.subscribe { event in + sub2Value = try! event.element!.wait() + }.disposed(by: db.disposeBag) + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + let expected = GraphQLResult( data: ["importantEmail": [ "inbox":[ @@ -328,62 +381,33 @@ class SubscriptionTests : XCTestCase { ] ]] ) - // Subscription 1 - let _ = subscription.subscribe { event in - XCTAssertEqual(try! event.element!.wait(), expected) - }.disposed(by: disposeBag) - // Subscription 2 - let _ = subscription.subscribe { event in - XCTAssertEqual(try! event.element!.wait(), expected) - }.disposed(by: disposeBag) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) + XCTAssertEqual(sub1Value, expected) + XCTAssertEqual(sub2Value, expected) } /// 'produces a payload per subscription event' func testPayloadPerEvent() throws { - let disposeBag = DisposeBag() let db = EmailDb() - let subscription = try defaultSubscription(db: db, query: nestedQuery) - - let expected = [ - GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 2, - "unread": 1 - ], - "email":[ - "subject": "Alright", - "from": "yuzhi@graphql.org" - ] - ]] - ), - GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 3, - "unread": 2 - ], - "email":[ - "subject": "Tools", - "from": "hyo@graphql.org" - ] - ]] - ), - ] - - var eventCounter = 0 + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + + var currentResult = GraphQLResult() let _ = subscription.subscribe { event in - XCTAssertEqual(try! event.element!.wait(), expected[eventCounter]) - eventCounter += 1 - }.disposed(by: disposeBag) + currentResult = try! event.element!.wait() + }.disposed(by: db.disposeBag) db.trigger(email: Email( from: "yuzhi@graphql.org", @@ -391,21 +415,62 @@ class SubscriptionTests : XCTestCase { message: "Tests are good", unread: true )) + XCTAssertEqual(currentResult, GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 2, + "unread": 1 + ], + "email":[ + "subject": "Alright", + "from": "yuzhi@graphql.org" + ] + ]] + )) + db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", unread: true )) - - XCTAssertEqual(eventCounter, 2) + XCTAssertEqual(currentResult, GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 3, + "unread": 2 + ], + "email":[ + "subject": "Tools", + "from": "hyo@graphql.org" + ] + ]] + )) } /// 'should not trigger when subscription is already done' func testNoTriggerAfterDone() throws { let db = EmailDb() - let subscription = try defaultSubscription(db: db, query: nestedQuery) - + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + + var currentResult = GraphQLResult() + let subscriber = subscription.subscribe { event in + currentResult = try! event.element!.wait() + } + let expected = GraphQLResult( data: ["importantEmail": [ "inbox":[ @@ -418,19 +483,14 @@ class SubscriptionTests : XCTestCase { ] ]] ) - - var eventCounter = 0 - let subscriber = subscription.subscribe { event in - XCTAssertEqual(try! event.element!.wait(), expected) - eventCounter += 1 - } - + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true )) + XCTAssertEqual(currentResult, expected) subscriber.dispose() @@ -441,7 +501,7 @@ class SubscriptionTests : XCTestCase { message: "I <3 making things", unread: true )) - XCTAssertEqual(eventCounter, 1) + XCTAssertEqual(currentResult, expected) } /// 'should not trigger when subscription is thrown' @@ -452,7 +512,6 @@ class SubscriptionTests : XCTestCase { /// 'should handle error during execution of source event' func testErrorDuringSubscription() throws { - let disposeBag = DisposeBag() let db = EmailDb() let schema = emailSchemaWithResolvers( @@ -471,7 +530,7 @@ class SubscriptionTests : XCTestCase { } ) - let subscription = try createSubscription(pubsub: db.publisher, schema: schema, query: """ + let subscription = try createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -480,42 +539,25 @@ class SubscriptionTests : XCTestCase { } } """) - - let expected = [ - GraphQLResult( - data: ["importantEmail": [ - "email":[ - "subject": "Hello" - ] - ]] - ), - GraphQLResult( // An error in execution is presented as such. - data: ["importantEmail": nil], - errors: [ - GraphQLError(message: "Never leave.") - ] - ), - GraphQLResult( // However that does not close the response event stream. Subsequent events are still executed. - data: ["importantEmail": [ - "email":[ - "subject": "Bonjour" - ] - ]] - ) - ] - - var eventCounter = 0 + + var currentResult = GraphQLResult() let _ = subscription.subscribe { event in - XCTAssertEqual(try! event.element!.wait(), expected[eventCounter]) - eventCounter += 1 - }.disposed(by: disposeBag) - + currentResult = try! event.element!.wait() + }.disposed(by: db.disposeBag) + db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Hello", message: "Tests are good", unread: true )) + XCTAssertEqual(currentResult, GraphQLResult( + data: ["importantEmail": [ + "email":[ + "subject": "Hello" + ] + ]] + )) // An error in execution is presented as such. db.trigger(email: Email( @@ -524,6 +566,12 @@ class SubscriptionTests : XCTestCase { message: "Tests are good", unread: true )) + XCTAssertEqual(currentResult, GraphQLResult( + data: ["importantEmail": nil], + errors: [ + GraphQLError(message: "Never leave.") + ] + )) // However that does not close the response event stream. Subsequent events are still executed. db.trigger(email: Email( @@ -532,8 +580,13 @@ class SubscriptionTests : XCTestCase { message: "Tests are good", unread: true )) - - XCTAssertEqual(eventCounter, 3) + XCTAssertEqual(currentResult, GraphQLResult( + data: ["importantEmail": [ + "email":[ + "subject": "Bonjour" + ] + ]] + )) } /// 'should pass through error thrown in source event stream' @@ -598,7 +651,6 @@ let InboxType = try! GraphQLObjectType( ), ] ) - let EmailEventType = try! GraphQLObjectType( name: "EmailEvent", fields: [ @@ -610,7 +662,6 @@ let EmailEventType = try! GraphQLObjectType( ) ] ) - let EmailQueryType = try! GraphQLObjectType( name: "Query", fields: [ @@ -626,29 +677,10 @@ let EmailQueryType = try! GraphQLObjectType( //let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) -let basicQuery = """ - subscription { - importantEmail - } -""" -let nestedQuery = """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } -""" - class EmailDb { var emails: [Email] let publisher: PublishSubject + let disposeBag: DisposeBag init() { emails = [ @@ -660,61 +692,38 @@ class EmailDb { ) ] publisher = PublishSubject() + disposeBag = DisposeBag() } + /// Adds a new email to the database and triggers all observers func trigger(email:Email) { emails.append(email) publisher.onNext(email) } -} - -/// Generates a default schema and resolvers, and returns the subscription -private func defaultSubscription( - db:EmailDb, - query:String, - variableValues: [String: Map] = [:] -) throws -> SubscriptionObservable { - - let schema = emailSchemaWithResolvers( - resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - let email = emailAny as! Email - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: db.emails) - )) - }, - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(db.publisher) - } - ) - return try createSubscription(pubsub: db.publisher, schema: schema, query: query, variableValues: variableValues) -} - -/// Generates a subscription from the given schema and query. It's expected that the database is managed by the caller. -private func createSubscription( - pubsub: Observable, - schema: GraphQLSchema, - query: String, - variableValues: [String: Map] = [:] -) throws -> SubscriptionObservable { - let document = try parse(source: query) - let subscriptionOrError = try subscribe( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, - schema: schema, - documentAST: document, - rootValue: Void(), - context: Void(), - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: nil - ).wait() - return try extractSubscriptionFromResult(subscriptionOrError) + /// Generates a subscription to the database using a default schema and resolvers + func subscription ( + query:String, + variableValues: [String: Map] = [:] + ) throws -> SubscriptionObservable { + let schema = emailSchemaWithResolvers( + resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + let email = emailAny as! Email + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: self.emails) + )) + }, + subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + return eventLoopGroup.next().makeSucceededFuture(self.publisher) + } + ) + + return try createSubscription(schema: schema, query: query, variableValues: variableValues) + } } +/// Generates an email schema with the specified resolve and subscribe methods private func emailSchemaWithResolvers(resolve: GraphQLFieldResolve? = nil, subscribe: GraphQLFieldResolve? = nil) -> GraphQLSchema { return try! GraphQLSchema( query: EmailQueryType, @@ -736,8 +745,28 @@ private func emailSchemaWithResolvers(resolve: GraphQLFieldResolve? = nil, subsc ) } -private func extractSubscriptionFromResult(_ subscriptionResult: SubscriptionResult) throws -> SubscriptionObservable { - switch subscriptionResult { +/// Generates a subscription from the given schema and query. It's expected that the resolver/database interactions are configured by the caller. +private func createSubscription( + schema: GraphQLSchema, + query: String, + variableValues: [String: Map] = [:] +) throws -> SubscriptionObservable { + let document = try parse(source: query) + let subscriptionOrError = try subscribe( + queryStrategy: SerialFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: SerialFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + schema: schema, + documentAST: document, + rootValue: Void(), + context: Void(), + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: nil + ).wait() + + switch subscriptionOrError { case .success(let subscription): return subscription case .failure(let error): From 7adab89950efe69d38cac2a75dc3b369747d56c2 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 17:50:05 -0700 Subject: [PATCH 22/36] Cleans up definition file changes --- Sources/GraphQL/Type/Definition.swift | 51 +-------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 08edee79..b4ea29cb 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -533,28 +533,13 @@ public struct GraphQLField { self.subscribe = nil } - public init( - type: GraphQLOutputType, - description: String? = nil, - deprecationReason: String? = nil, - args: GraphQLArgumentConfigMap = [:], - resolve: GraphQLFieldResolve? - ) { - self.type = type - self.args = args - self.deprecationReason = deprecationReason - self.description = description - self.resolve = resolve - self.subscribe = nil - } - public init( type: GraphQLOutputType, description: String? = nil, deprecationReason: String? = nil, args: GraphQLArgumentConfigMap = [:], resolve: GraphQLFieldResolve?, - subscribe: GraphQLFieldResolve? + subscribe: GraphQLFieldResolve? = nil ) { self.type = type self.args = args @@ -564,21 +549,6 @@ public struct GraphQLField { self.subscribe = subscribe } - public init( - type: GraphQLOutputType, - description: String? = nil, - deprecationReason: String? = nil, - args: GraphQLArgumentConfigMap = [:], - subscribe: GraphQLFieldResolve? - ) { - self.type = type - self.args = args - self.deprecationReason = deprecationReason - self.description = description - self.resolve = nil - self.subscribe = subscribe - } - public init( type: GraphQLOutputType, description: String? = nil, @@ -597,25 +567,6 @@ public struct GraphQLField { } self.subscribe = nil } - -// public init( -// type: GraphQLOutputType, -// description: String? = nil, -// deprecationReason: String? = nil, -// args: GraphQLArgumentConfigMap = [:], -// subscribe: GraphQLFieldResolveInput -// ) { -// self.type = type -// self.args = args -// self.deprecationReason = deprecationReason -// self.description = description -// -// self.resolve = nil -// self.subscribe = { source, args, context, eventLoopGroup, info in -// let result = try subscribe(source, args, context, info) -// return eventLoopGroup.next().makeSucceededFuture(result) -// } -// } } public typealias GraphQLFieldDefinitionMap = [String: GraphQLFieldDefinition] From aee8841cf6af3a807a1e8cd0145cbf23df29aa08 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 22:38:40 -0700 Subject: [PATCH 23/36] Adds handling for multiple errors --- Sources/GraphQL/Subscription/Subscribe.swift | 67 ++++++++++++------- .../Subscription/SubscriptionTests.swift | 29 ++++---- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index c1cd2245..fdfb4d99 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -53,10 +53,9 @@ func subscribe( operationName: operationName ) - return sourceFuture.map{ subscriptionResult -> SubscriptionResult in - do { - let subscriptionObserver = try subscriptionResult.get() - let eventObserver = subscriptionObserver.map { eventPayload -> Future in + return sourceFuture.map{ sourceResult -> SubscriptionResult in + if let sourceObservable = sourceResult.observable { + let subscriptionObservable = sourceObservable.map { eventPayload -> Future in // For each payload yielded from a subscription, map it over the normal // GraphQL `execute` function, with `payload` as the rootValue. @@ -78,11 +77,9 @@ func subscribe( operationName: operationName ) } - return SubscriptionResult.success(eventObserver) - } catch let graphQLError as GraphQLError { - return SubscriptionResult.failure(graphQLError) - } catch let error { - return SubscriptionResult.failure(GraphQLError(error)) + return SubscriptionResult(observable: subscriptionObservable) + } else { + return SubscriptionResult(errors: sourceResult.errors) } } } @@ -164,9 +161,9 @@ func createSourceEventStream( result: nil ) - return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult.failure(error)) + return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult(errors: [error])) } catch { - return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult.failure(GraphQLError(error))) + return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult(errors: [GraphQLError(error)])) } } @@ -192,7 +189,7 @@ func executeSubscription( let fieldNode = fieldNodes.first! guard let fieldDef = getFieldDef(schema: context.schema, parentType: type, fieldAST: fieldNode) else { - throw GraphQLError.init( + throw GraphQLError( message: "`The subscription field '\(fieldNode.name.value)' is not defined.`", nodes: fieldNodes ) @@ -237,34 +234,56 @@ func executeSubscription( let resolvedFuture:Future switch resolvedFutureOrError { case let .failure(error): - throw error + if let graphQLError = error as? GraphQLError { + throw graphQLError + } else { + throw GraphQLError(error) + } case let .success(success): resolvedFuture = success } return resolvedFuture.map { resolved -> SourceEventStreamResult in if !context.errors.isEmpty { - // TODO improve this to return multiple errors if we have them. - return SourceEventStreamResult.failure(context.errors.first!) + return SourceEventStreamResult(errors: context.errors) } else if let error = resolved as? GraphQLError { - return SourceEventStreamResult.failure(error) + return SourceEventStreamResult(errors: [error]) } else if let observable = resolved as? SourceEventStreamObservable { - return SourceEventStreamResult.success(observable) + return SourceEventStreamResult(observable: observable) } else if resolved == nil { - return SourceEventStreamResult.failure( + return SourceEventStreamResult(errors: [ GraphQLError(message: "Resolved subscription was nil") - ) + ]) } else { let resolvedObj = resolved as AnyObject - return SourceEventStreamResult.failure( + return SourceEventStreamResult(errors: [ GraphQLError( message: "Subscription field resolver must return SourceEventStreamObservable. Received: '\(resolvedObj)'" ) - ) + ]) } } } -typealias SubscriptionObservable = Observable> -typealias SubscriptionResult = Result +public struct SubscriptionResult { + public var observable: SubscriptionObservable? + public var errors: [GraphQLError] + + public init(observable: SubscriptionObservable? = nil, errors: [GraphQLError] = []) { + self.observable = observable + self.errors = errors + } +} +public typealias SubscriptionObservable = Observable> + +struct SourceEventStreamResult { + public var observable: SourceEventStreamObservable? + public var errors: [GraphQLError] + + public init(observable: SourceEventStreamObservable? = nil, errors: [GraphQLError] = []) { + self.observable = observable + self.errors = errors + } +} typealias SourceEventStreamObservable = Observable -typealias SourceEventStreamResult = Result + + diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index c5d4d21a..1297d2fc 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -172,9 +172,9 @@ class SubscriptionTests : XCTestCase { """ ) ) { error in - let graphQlError = error as! GraphQLError - XCTAssertEqual(graphQlError.message, "`The subscription field 'unknownField' is not defined.`") - XCTAssertEqual(graphQlError.locations, [SourceLocation(line: 2, column: 5)]) + let graphQLError = error as! GraphQLError + XCTAssertEqual(graphQLError.message, "`The subscription field 'unknownField' is not defined.`") + XCTAssertEqual(graphQLError.locations, [SourceLocation(line: 2, column: 5)]) } } @@ -203,9 +203,9 @@ class SubscriptionTests : XCTestCase { } """) ) { error in - let graphQlError = error as! GraphQLError + let graphQLError = error as! GraphQLError XCTAssertEqual( - graphQlError.message, + graphQLError.message, "Subscription field resolver must return SourceEventStreamObservable. Received: 'test'" ) } @@ -221,8 +221,8 @@ class SubscriptionTests : XCTestCase { } """) ) { error in - let graphQlError = error as! GraphQLError - XCTAssertEqual(graphQlError.message, "test error") + let graphQLError = error as! GraphQLError + XCTAssertEqual(graphQLError.message, "test error") } } @@ -278,9 +278,9 @@ class SubscriptionTests : XCTestCase { ] ) ) { error in - let graphQlError = error as! GraphQLError + let graphQLError = error as! GraphQLError XCTAssertEqual( - graphQlError.message, + graphQLError.message, "Variable \"$priority\" got invalid value \"meow\".\nExpected type \"Int\", found \"meow\"." ) } @@ -752,7 +752,7 @@ private func createSubscription( variableValues: [String: Map] = [:] ) throws -> SubscriptionObservable { let document = try parse(source: query) - let subscriptionOrError = try subscribe( + let result = try subscribe( queryStrategy: SerialFieldExecutionStrategy(), mutationStrategy: SerialFieldExecutionStrategy(), subscriptionStrategy: SerialFieldExecutionStrategy(), @@ -766,10 +766,9 @@ private func createSubscription( operationName: nil ).wait() - switch subscriptionOrError { - case .success(let subscription): - return subscription - case .failure(let error): - throw error + if let observable = result.observable { + return observable + } else { + throw result.errors.first! // We may have more than one... } } From 83632df6616285f1401f6a2270c0de9384ceff60 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 23:00:20 -0700 Subject: [PATCH 24/36] Adds justification for forced unwrapping --- Sources/GraphQL/Subscription/Subscribe.swift | 16 +++++++++------- .../Subscription/SubscriptionTests.swift | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index fdfb4d99..ca5a357b 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -183,14 +183,14 @@ func executeSubscription( fields: &inputFields, visitedFragmentNames: &visitedFragmentNames ) - let responseNames = fields.keys - let responseName = responseNames.first! // TODO add error handling here + // If query is valid, fields is guaranteed to have at least 1 member + let responseName = fields.keys.first! let fieldNodes = fields[responseName]! let fieldNode = fieldNodes.first! guard let fieldDef = getFieldDef(schema: context.schema, parentType: type, fieldAST: fieldNode) else { throw GraphQLError( - message: "`The subscription field '\(fieldNode.name.value)' is not defined.`", + message: "The subscription field '\(fieldNode.name.value)' is not defined.", nodes: fieldNodes ) } @@ -264,20 +264,22 @@ func executeSubscription( } } +/// SubscriptionResult wraps the observable and error data returned by the subscribe request. public struct SubscriptionResult { - public var observable: SubscriptionObservable? - public var errors: [GraphQLError] + public let observable: SubscriptionObservable? + public let errors: [GraphQLError] public init(observable: SubscriptionObservable? = nil, errors: [GraphQLError] = []) { self.observable = observable self.errors = errors } } +/// SubscriptionObservable represents an event stream of fully resolved GraphQL subscription results. It can be used to add subscribers to this stream public typealias SubscriptionObservable = Observable> struct SourceEventStreamResult { - public var observable: SourceEventStreamObservable? - public var errors: [GraphQLError] + public let observable: SourceEventStreamObservable? + public let errors: [GraphQLError] public init(observable: SourceEventStreamObservable? = nil, errors: [GraphQLError] = []) { self.observable = observable diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index 1297d2fc..c76160d7 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -173,7 +173,7 @@ class SubscriptionTests : XCTestCase { ) ) { error in let graphQLError = error as! GraphQLError - XCTAssertEqual(graphQLError.message, "`The subscription field 'unknownField' is not defined.`") + XCTAssertEqual(graphQLError.message, "The subscription field 'unknownField' is not defined.") XCTAssertEqual(graphQLError.locations, [SourceLocation(line: 2, column: 5)]) } } From 471791dd488c5beb66a91e39eeb7fc6024aa5c3a Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 22 Feb 2021 23:18:55 -0700 Subject: [PATCH 25/36] Updates documentation to reflect implementation --- Sources/GraphQL/Subscription/Subscribe.swift | 42 ++++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index ca5a357b..140b7738 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -6,21 +6,20 @@ import NIO /** * Implements the "Subscribe" algorithm described in the GraphQL specification. * - * Returns a Promise which resolves to either an AsyncIterator (if successful) - * or an ExecutionResult (error). The promise will be rejected if the schema or - * other arguments to this function are invalid, or if the resolved event stream - * is not an async iterable. + * Returns a future which resolves to a SubscriptionResult containing either + * a SubscriptionObservable (if successful), or GraphQLErrors (error). * * If the client-provided arguments to this function do not result in a - * compliant subscription, a GraphQL Response (ExecutionResult) with - * descriptive errors and no data will be returned. + * compliant subscription, the future will resolve to a + * SubscriptionResult containing `errors` and no `observable`. * * If the source stream could not be created due to faulty subscription - * resolver logic or underlying systems, the promise will resolve to a single - * ExecutionResult containing `errors` and no `data`. + * resolver logic or underlying systems, the future will resolve to a + * SubscriptionResult containing `errors` and no `observable`. * - * If the operation succeeded, the promise resolves to an AsyncIterator, which - * yields a stream of ExecutionResults representing the response stream. + * If the operation succeeded, the future will resolve to a SubscriptionResult, + * containing an `observable` which yields a stream of GraphQLResults + * representing the response stream. * * Accepts either an object with named arguments, or individual arguments. */ @@ -77,7 +76,7 @@ func subscribe( operationName: operationName ) } - return SubscriptionResult(observable: subscriptionObservable) + return SubscriptionResult(observable: subscriptionObservable, errors: sourceResult.errors) } else { return SubscriptionResult(errors: sourceResult.errors) } @@ -88,21 +87,20 @@ func subscribe( * Implements the "CreateSourceEventStream" algorithm described in the * GraphQL specification, resolving the subscription source event stream. * - * Returns a Promise which resolves to either an AsyncIterable (if successful) - * or an ExecutionResult (error). The promise will be rejected if the schema or - * other arguments to this function are invalid, or if the resolved event stream - * is not an async iterable. + * Returns a Future which resolves to a SourceEventStreamResult, containing + * either an Observable (if successful) or GraphQLErrors (error). * * If the client-provided arguments to this function do not result in a - * compliant subscription, a GraphQL Response (ExecutionResult) with - * descriptive errors and no data will be returned. + * compliant subscription, the future will resolve to a + * SourceEventStreamResult containing `errors` and no `observable`. * - * If the the source stream could not be created due to faulty subscription - * resolver logic or underlying systems, the promise will resolve to a single - * ExecutionResult containing `errors` and no `data`. + * If the source stream could not be created due to faulty subscription + * resolver logic or underlying systems, the future will resolve to a + * SourceEventStreamResult containing `errors` and no `observable`. * - * If the operation succeeded, the promise resolves to the AsyncIterable for the - * event stream returned by the resolver. + * If the operation succeeded, the future will resolve to a SubscriptionResult, + * containing an `observable` which yields a stream of event objects + * returned by the subscription resolver. * * A Source Event Stream represents a sequence of events, each of which triggers * a GraphQL execution for that event. From 292091a3e7c9655bda0c50f10f0c5b23c62233dc Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 23 Feb 2021 09:49:22 -0700 Subject: [PATCH 26/36] Removes unnecessary function --- Sources/GraphQL/Execution/Execute.swift | 23 ------------------ Sources/GraphQL/Subscription/Subscribe.swift | 25 +++++++++++++------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 1a6eb56d..62acdb25 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -1229,26 +1229,3 @@ func getFieldDef( // we know this field exists because we passed validation before execution return parentType.fields[fieldName]! } - -func buildResolveInfo( - context: ExecutionContext, - fieldDef: GraphQLFieldDefinition, - fieldASTs: [Field], - parentType: GraphQLObjectType, - path: IndexPath -) -> GraphQLResolveInfo { - // The resolve function's optional fourth argument is a collection of - // information about the current execution state. - return GraphQLResolveInfo.init( - fieldName: fieldDef.name, - fieldASTs: fieldASTs, - returnType: fieldDef.type, - parentType: parentType, - path: path, - schema: context.schema, - fragments: context.fragments, - rootValue: context.rootValue, - operation: context.operation, - variableValues: context.variableValues - ) -} diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 140b7738..9891d61d 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -193,15 +193,6 @@ func executeSubscription( ) } - let path = IndexPath.init().appending(fieldNode.name.value) - let info = buildResolveInfo( - context: context, - fieldDef: fieldDef, - fieldASTs: fieldNodes, - parentType: type, - path: path - ) - // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. // It differs from "ResolveFieldValue" due to providing a different `resolveFn`. @@ -213,6 +204,22 @@ func executeSubscription( // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. let contextValue = context.context + + // The resolve function's optional fourth argument is a collection of + // information about the current execution state. + let path = IndexPath.init().appending(fieldNode.name.value) + let info = GraphQLResolveInfo.init( + fieldName: fieldDef.name, + fieldASTs: fieldNodes, + returnType: fieldDef.type, + parentType: type, + path: path, + schema: context.schema, + fragments: context.fragments, + rootValue: context.rootValue, + operation: context.operation, + variableValues: context.variableValues + ) // Call the `subscribe()` resolver or the default resolver to produce an // Observable yielding raw payloads. From c6da6b82bbb18cb1e870e6f1f42ff7b4984ae326 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 23 Feb 2021 10:31:50 -0700 Subject: [PATCH 27/36] Adds primary subscription entry point function --- Sources/GraphQL/GraphQL.swift | 64 ++++++++++++++++ .../Subscription/SubscriptionTests.swift | 76 ++++++++++++++++--- 2 files changed, 131 insertions(+), 9 deletions(-) diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 3c85c176..45af3957 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -1,5 +1,6 @@ import Foundation import NIO +import RxSwift public struct GraphQLResult : Equatable, Codable, CustomStringConvertible { public var data: Map? @@ -151,3 +152,66 @@ public func graphql( ) } } + +/// This is the primary entry point function for fulfilling GraphQL subscription +/// operations by parsing, validating, and executing a GraphQL subscription +/// document along side a GraphQL schema. +/// +/// More sophisticated GraphQL servers, such as those which persist queries, +/// may wish to separate the validation and execution phases to a static time +/// tooling step, and a server runtime step. +/// +/// - parameter queryStrategy: The field execution strategy to use for query requests +/// - parameter mutationStrategy: The field execution strategy to use for mutation requests +/// - parameter subscriptionStrategy: The field execution strategy to use for subscription requests +/// - parameter instrumentation: The instrumentation implementation to call during the parsing, validating, execution, and field resolution stages. +/// - parameter schema: The GraphQL type system to use when validating and executing a query. +/// - parameter request: A GraphQL language formatted string representing the requested operation. +/// - parameter rootValue: The value provided as the first argument to resolver functions on the top level type (e.g. the query object type). +/// - parameter contextValue: A context value provided to all resolver functions +/// - parameter variableValues: A mapping of variable name to runtime value to use for all variables defined in the `request`. +/// - parameter operationName: The name of the operation to use if `request` contains multiple possible operations. Can be omitted if `request` contains only one operation. +/// +/// - throws: throws GraphQLError if an error occurs while parsing the `request`. +/// +/// - returns: returns a SubscriptionResult containing the subscription observable inside the key `observable` and any validation or execution errors inside the key `errors`. The +/// value of `observable` might be `null` if, for example, the query is invalid. It's not possible to have both `observable` and `errors`. The observable payloads are +/// GraphQLResults which contain the result of the query inside the key `data` and any validation or execution errors inside the key `errors`. The value of `data` might be `null`. +/// It's possible to have both `data` and `errors` if an error occurs only in a specific field. If that happens the value of that field will be `null` and there +/// will be an error inside `errors` specifying the reason for the failure and the path of the failed field. +public func graphqlSubscribe( + queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), + mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), + subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), + instrumentation: Instrumentation = NoOpInstrumentation, + schema: GraphQLSchema, + request: String, + rootValue: Any = Void(), + context: Any = Void(), + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil +) throws -> Future { + + let source = Source(body: request, name: "GraphQL Subscription request") + let documentAST = try parse(instrumentation: instrumentation, source: source) + let validationErrors = validate(instrumentation: instrumentation, schema: schema, ast: documentAST) + + guard validationErrors.isEmpty else { + return eventLoopGroup.next().makeSucceededFuture(SubscriptionResult(errors: validationErrors)) + } + + return subscribe( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + documentAST: documentAST, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ) +} diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index c76160d7..bc00c203 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -5,7 +5,62 @@ import RxSwift /// This follows the graphql-js testing, with deviations where noted. class SubscriptionTests : XCTestCase { - + + // MARK: Test primary graphqlSubscribe function + + /// This is not present in graphql-js. + func testGraphqlSubscribe() throws { + let db = EmailDb() + let schema = db.defaultSchema() + let query = """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """ + + let subscriptionResult = try graphqlSubscribe( + schema: schema, + request: query, + eventLoopGroup: eventLoopGroup + ).wait() + + let observable = subscriptionResult.observable! + + var currentResult = GraphQLResult() + let _ = observable.subscribe { event in + currentResult = try! event.element!.wait() + }.disposed(by: db.disposeBag) + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + + XCTAssertEqual(currentResult, GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 2, + "unread": 1 + ], + "email":[ + "subject": "Alright", + "from": "yuzhi@graphql.org" + ] + ]] + )) + } + // MARK: Subscription Initialization Phase /// accepts multiple subscription fields defined in schema @@ -701,12 +756,9 @@ class EmailDb { publisher.onNext(email) } - /// Generates a subscription to the database using a default schema and resolvers - func subscription ( - query:String, - variableValues: [String: Map] = [:] - ) throws -> SubscriptionObservable { - let schema = emailSchemaWithResolvers( + /// Returns the default email schema, with standard resolvers. + func defaultSchema() -> GraphQLSchema { + return emailSchemaWithResolvers( resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in let email = emailAny as! Email return eventLoopGroup.next().makeSucceededFuture(EmailEvent( @@ -718,8 +770,14 @@ class EmailDb { return eventLoopGroup.next().makeSucceededFuture(self.publisher) } ) - - return try createSubscription(schema: schema, query: query, variableValues: variableValues) + } + + /// Generates a subscription to the database using the default schema and resolvers + func subscription ( + query:String, + variableValues: [String: Map] = [:] + ) throws -> SubscriptionObservable { + return try createSubscription(schema: defaultSchema(), query: query, variableValues: variableValues) } } From 367e5a1b6819e6013fb1db0cfd700e322bef9d30 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 23 Feb 2021 10:57:29 -0700 Subject: [PATCH 28/36] Removes thread deadlocking comment --- Tests/GraphQLTests/Subscription/SubscriptionTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index bc00c203..46253cc6 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -728,8 +728,6 @@ let EmailQueryType = try! GraphQLObjectType( // MARK: Test Helpers -// TODO: I seem to be getting some thread deadlocking when I set this to system count. FIX -//let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) class EmailDb { From baba1bcc606c137731835bd69c062c433b5dc504 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 23 Feb 2021 15:24:46 -0700 Subject: [PATCH 29/36] Adds testing of subscription arguments --- .../Subscription/SubscriptionTests.swift | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index 46253cc6..3250803e 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -502,6 +502,83 @@ class SubscriptionTests : XCTestCase { ]] )) } + + /// Tests that subscriptions use arguments correctly. + /// This is not in the graphql-js tests. + func testArguments() throws { + let db = EmailDb() + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 5) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + + var currentResult = GraphQLResult() + let _ = subscription.subscribe { event in + currentResult = try! event.element!.wait() + }.disposed(by: db.disposeBag) + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true, + priority: 7 + )) + let firstMessageExpected = GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 2, + "unread": 1 + ], + "email":[ + "subject": "Alright", + "from": "yuzhi@graphql.org" + ] + ]] + ) + XCTAssertEqual(currentResult, firstMessageExpected) + + // Low priority email shouldn't trigger an event + db.trigger(email: Email( + from: "hyo@graphql.org", + subject: "Not Important", + message: "Ignore this email", + unread: true, + priority: 2 + )) + XCTAssertEqual(currentResult, firstMessageExpected) + + // Higher priority one should trigger again + db.trigger(email: Email( + from: "hyo@graphql.org", + subject: "Tools", + message: "I <3 making things", + unread: true, + priority: 5 + )) + XCTAssertEqual(currentResult, GraphQLResult( + data: ["importantEmail": [ + "inbox":[ + "total": 4, + "unread": 3 + ], + "email":[ + "subject": "Tools", + "from": "hyo@graphql.org" + ] + ]] + )) + } /// 'should not trigger when subscription is already done' func testNoTriggerAfterDone() throws { @@ -657,6 +734,15 @@ struct Email : Encodable { let subject:String let message:String let unread:Bool + let priority:Int + + init(from:String, subject:String, message:String, unread:Bool, priority:Int = 0) { + self.from = from + self.subject = subject + self.message = message + self.unread = unread + self.priority = priority + } } struct Inbox : Encodable { @@ -764,8 +850,13 @@ class EmailDb { inbox: Inbox(emails: self.emails) )) }, - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(self.publisher) + subscribe: {_, args, _, eventLoopGroup, _ throws -> EventLoopFuture in + let priority = args["priority"].int ?? 0 + let filtered = self.publisher.filter { emailAny in + let email = emailAny as! Email + return email.priority >= priority + } + return eventLoopGroup.next().makeSucceededFuture(filtered) } ) } From 6728e3168dd07cf523490cd1e4829cc7541c48ed Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 24 Feb 2021 10:26:27 -0700 Subject: [PATCH 30/36] Adds test for observable type checking --- Sources/GraphQL/Subscription/Subscribe.swift | 1 + .../Subscription/SubscriptionTests.swift | 55 ++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 9891d61d..96fdfedc 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -291,6 +291,7 @@ struct SourceEventStreamResult { self.errors = errors } } +/// Observables MUST be declared as 'Any' due to Swift not having covariant generic support. Resolvers should handle type checks. typealias SourceEventStreamObservable = Observable diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index 3250803e..f4705a09 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -726,6 +726,39 @@ class SubscriptionTests : XCTestCase { /// 'should resolve GraphQL error from source event stream' // Not necessary - Pub/sub implementation handles event erroring + + /// Test incorrect observable publish type errors + func testErrorWrongObservableType() throws { + let db = EmailDb() + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + + var currentResult = GraphQLResult() + let _ = subscription.subscribe { event in + currentResult = try! event.element!.wait() + }.disposed(by: db.disposeBag) + + db.publisher.onNext("String instead of email") + + XCTAssertEqual(currentResult, GraphQLResult( + data: ["importantEmail": nil], + errors: [ + GraphQLError(message: "String is not Email") + ] + )) + } } // MARK: Types @@ -844,17 +877,23 @@ class EmailDb { func defaultSchema() -> GraphQLSchema { return emailSchemaWithResolvers( resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - let email = emailAny as! Email - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: self.emails) - )) + if let email = emailAny as? Email { + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: self.emails) + )) + } else { + throw GraphQLError(message: "\(type(of:emailAny)) is not Email") + } }, subscribe: {_, args, _, eventLoopGroup, _ throws -> EventLoopFuture in let priority = args["priority"].int ?? 0 - let filtered = self.publisher.filter { emailAny in - let email = emailAny as! Email - return email.priority >= priority + let filtered = self.publisher.filter { emailAny throws in + if let email = emailAny as? Email { + return email.priority >= priority + } else { + return true + } } return eventLoopGroup.next().makeSucceededFuture(filtered) } From fbe96f6be046ce9a20ad6ddfa77071533d630efd Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 2 Mar 2021 16:33:30 -0700 Subject: [PATCH 31/36] Minor final cleanup --- Sources/GraphQL/GraphQL.swift | 13 +++++++++ Sources/GraphQL/Subscription/Subscribe.swift | 28 +++++-------------- .../Subscription/SubscriptionTests.swift | 5 ++-- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 45af3957..cadf4994 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -40,6 +40,19 @@ public struct GraphQLResult : Equatable, Codable, CustomStringConvertible { } } +/// SubscriptionResult wraps the observable and error data returned by the subscribe request. +public struct SubscriptionResult { + public let observable: SubscriptionObservable? + public let errors: [GraphQLError] + + public init(observable: SubscriptionObservable? = nil, errors: [GraphQLError] = []) { + self.observable = observable + self.errors = errors + } +} +/// SubscriptionObservable represents an event stream of fully resolved GraphQL subscription results. Subscribers can be added to this stream. +public typealias SubscriptionObservable = Observable> + /// This is the primary entry point function for fulfilling GraphQL operations /// by parsing, validating, and executing a GraphQL document along side a /// GraphQL schema. diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 96fdfedc..4a396106 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -37,7 +37,6 @@ func subscribe( operationName: String? = nil ) -> EventLoopFuture { - let sourceFuture = createSourceEventStream( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, @@ -252,7 +251,7 @@ func executeSubscription( return SourceEventStreamResult(errors: context.errors) } else if let error = resolved as? GraphQLError { return SourceEventStreamResult(errors: [error]) - } else if let observable = resolved as? SourceEventStreamObservable { + } else if let observable = resolved as? SourceEventObservable { return SourceEventStreamResult(observable: observable) } else if resolved == nil { return SourceEventStreamResult(errors: [ @@ -262,36 +261,23 @@ func executeSubscription( let resolvedObj = resolved as AnyObject return SourceEventStreamResult(errors: [ GraphQLError( - message: "Subscription field resolver must return SourceEventStreamObservable. Received: '\(resolvedObj)'" + message: "Subscription field resolver must return SourceEventObservable. Received: '\(resolvedObj)'" ) ]) } } } -/// SubscriptionResult wraps the observable and error data returned by the subscribe request. -public struct SubscriptionResult { - public let observable: SubscriptionObservable? - public let errors: [GraphQLError] - - public init(observable: SubscriptionObservable? = nil, errors: [GraphQLError] = []) { - self.observable = observable - self.errors = errors - } -} -/// SubscriptionObservable represents an event stream of fully resolved GraphQL subscription results. It can be used to add subscribers to this stream -public typealias SubscriptionObservable = Observable> - struct SourceEventStreamResult { - public let observable: SourceEventStreamObservable? + public let observable: SourceEventObservable? public let errors: [GraphQLError] - public init(observable: SourceEventStreamObservable? = nil, errors: [GraphQLError] = []) { + public init(observable: SourceEventObservable? = nil, errors: [GraphQLError] = []) { self.observable = observable self.errors = errors } } -/// Observables MUST be declared as 'Any' due to Swift not having covariant generic support. Resolvers should handle type checks. -typealias SourceEventStreamObservable = Observable - +// Subscription resolvers MUST return observables that are declared as 'Any' due to Swift not having covariant generic support for type +// checking. Normal resolvers for subscription fields should handle type casting, same as resolvers for query fields. +typealias SourceEventObservable = Observable diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index f4705a09..d06230c5 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -8,7 +8,7 @@ class SubscriptionTests : XCTestCase { // MARK: Test primary graphqlSubscribe function - /// This is not present in graphql-js. + /// This test is not present in graphql-js, but just tests basic functionality. func testGraphqlSubscribe() throws { let db = EmailDb() let schema = db.defaultSchema() @@ -261,7 +261,7 @@ class SubscriptionTests : XCTestCase { let graphQLError = error as! GraphQLError XCTAssertEqual( graphQLError.message, - "Subscription field resolver must return SourceEventStreamObservable. Received: 'test'" + "Subscription field resolver must return SourceEventObservable. Received: 'test'" ) } } @@ -462,6 +462,7 @@ class SubscriptionTests : XCTestCase { var currentResult = GraphQLResult() let _ = subscription.subscribe { event in currentResult = try! event.element!.wait() + print(currentResult) }.disposed(by: db.disposeBag) db.trigger(email: Email( From f1d9ece2103ca47ec475fddb5bb17f16dc66402b Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 10 Mar 2021 00:18:48 -0700 Subject: [PATCH 32/36] Adds wrapper class for Observables, generalizes --- Sources/GraphQL/GraphQL.swift | 8 +-- .../GraphQL/Subscription/EventStream.swift | 26 +++++++++ Sources/GraphQL/Subscription/Subscribe.swift | 24 ++++----- .../Subscription/SubscriptionTests.swift | 54 +++++++++---------- 4 files changed, 68 insertions(+), 44 deletions(-) create mode 100644 Sources/GraphQL/Subscription/EventStream.swift diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index cadf4994..e0a2ac83 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -42,16 +42,16 @@ public struct GraphQLResult : Equatable, Codable, CustomStringConvertible { /// SubscriptionResult wraps the observable and error data returned by the subscribe request. public struct SubscriptionResult { - public let observable: SubscriptionObservable? + public let stream: SubscriptionEventStream? public let errors: [GraphQLError] - public init(observable: SubscriptionObservable? = nil, errors: [GraphQLError] = []) { - self.observable = observable + public init(stream: SubscriptionEventStream? = nil, errors: [GraphQLError] = []) { + self.stream = stream self.errors = errors } } /// SubscriptionObservable represents an event stream of fully resolved GraphQL subscription results. Subscribers can be added to this stream. -public typealias SubscriptionObservable = Observable> +public typealias SubscriptionEventStream = EventStream> /// This is the primary entry point function for fulfilling GraphQL operations /// by parsing, validating, and executing a GraphQL document along side a diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift new file mode 100644 index 00000000..6166ae74 --- /dev/null +++ b/Sources/GraphQL/Subscription/EventStream.swift @@ -0,0 +1,26 @@ +// Copyright (c) 2021 PassiveLogic, Inc. + +import RxSwift + +public class EventStream { + /// This class should be overridden + func transform(_ closure: @escaping (Element) throws -> To) -> EventStream { + fatalError("This function should be overridden by implementing classes") + } +} + +//extension Observable: EventStream { +// func transform(_ closure: @escaping (Element) throws -> To) -> EventStream { +// return self.map(closure) +// } +//} + +public class ObservableEventStream : EventStream { + var observable: Observable + init(observable: Observable) { + self.observable = observable + } + override func transform(_ closure: @escaping (Element) throws -> To) -> EventStream { + return ObservableEventStream(observable: observable.map(closure)) + } +} diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 4a396106..370dde42 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -52,8 +52,8 @@ func subscribe( ) return sourceFuture.map{ sourceResult -> SubscriptionResult in - if let sourceObservable = sourceResult.observable { - let subscriptionObservable = sourceObservable.map { eventPayload -> Future in + if let sourceStream = sourceResult.stream { + let subscriptionStream = sourceStream.transform { eventPayload -> Future in // For each payload yielded from a subscription, map it over the normal // GraphQL `execute` function, with `payload` as the rootValue. @@ -75,7 +75,7 @@ func subscribe( operationName: operationName ) } - return SubscriptionResult(observable: subscriptionObservable, errors: sourceResult.errors) + return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) } else { return SubscriptionResult(errors: sourceResult.errors) } @@ -251,8 +251,8 @@ func executeSubscription( return SourceEventStreamResult(errors: context.errors) } else if let error = resolved as? GraphQLError { return SourceEventStreamResult(errors: [error]) - } else if let observable = resolved as? SourceEventObservable { - return SourceEventStreamResult(observable: observable) + } else if let stream = resolved as? EventStream { + return SourceEventStreamResult(stream: stream) } else if resolved == nil { return SourceEventStreamResult(errors: [ GraphQLError(message: "Resolved subscription was nil") @@ -261,23 +261,21 @@ func executeSubscription( let resolvedObj = resolved as AnyObject return SourceEventStreamResult(errors: [ GraphQLError( - message: "Subscription field resolver must return SourceEventObservable. Received: '\(resolvedObj)'" + message: "Subscription field resolver must return EventStream. Received: '\(resolvedObj)'" ) ]) } } } +// Subscription resolvers MUST return observables that are declared as 'Any' due to Swift not having covariant generic support for type +// checking. Normal resolvers for subscription fields should handle type casting, same as resolvers for query fields. struct SourceEventStreamResult { - public let observable: SourceEventObservable? + public let stream: EventStream? public let errors: [GraphQLError] - public init(observable: SourceEventObservable? = nil, errors: [GraphQLError] = []) { - self.observable = observable + public init(stream: EventStream? = nil, errors: [GraphQLError] = []) { + self.stream = stream self.errors = errors } } - -// Subscription resolvers MUST return observables that are declared as 'Any' due to Swift not having covariant generic support for type -// checking. Normal resolvers for subscription fields should handle type casting, same as resolvers for query fields. -typealias SourceEventObservable = Observable diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index d06230c5..fc52a321 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -32,11 +32,11 @@ class SubscriptionTests : XCTestCase { request: query, eventLoopGroup: eventLoopGroup ).wait() - - let observable = subscriptionResult.observable! + print(subscriptionResult) + let subscription = subscriptionResult.stream! as! ObservableEventStream> var currentResult = GraphQLResult() - let _ = observable.subscribe { event in + let _ = subscription.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -123,10 +123,10 @@ class SubscriptionTests : XCTestCase { } } } - """) + """) as! ObservableEventStream> var currentResult = GraphQLResult() - let _ = subscription.subscribe { event in + let _ = subscription.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -193,9 +193,9 @@ class SubscriptionTests : XCTestCase { importantEmail notImportantEmail } - """) + """) as! ObservableEventStream> - let _ = subscription.subscribe{ event in + let _ = subscription.observable.subscribe{ event in let _ = try! event.element!.wait() }.disposed(by: db.disposeBag) db.trigger(email: Email( @@ -261,7 +261,7 @@ class SubscriptionTests : XCTestCase { let graphQLError = error as! GraphQLError XCTAssertEqual( graphQLError.message, - "Subscription field resolver must return SourceEventObservable. Received: 'test'" + "Subscription field resolver must return EventStream. Received: 'test'" ) } } @@ -360,10 +360,10 @@ class SubscriptionTests : XCTestCase { } } } - """) + """) as! ObservableEventStream> var currentResult = GraphQLResult() - let _ = subscription.subscribe { event in + let _ = subscription.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -403,17 +403,17 @@ class SubscriptionTests : XCTestCase { } } } - """) + """) as! ObservableEventStream> // Subscription 1 var sub1Value = GraphQLResult() - let _ = subscription.subscribe { event in + let _ = subscription.observable.subscribe { event in sub1Value = try! event.element!.wait() }.disposed(by: db.disposeBag) // Subscription 2 var sub2Value = GraphQLResult() - let _ = subscription.subscribe { event in + let _ = subscription.observable.subscribe { event in sub2Value = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -457,10 +457,10 @@ class SubscriptionTests : XCTestCase { } } } - """) + """) as! ObservableEventStream> var currentResult = GraphQLResult() - let _ = subscription.subscribe { event in + let _ = subscription.observable.subscribe { event in currentResult = try! event.element!.wait() print(currentResult) }.disposed(by: db.disposeBag) @@ -521,10 +521,10 @@ class SubscriptionTests : XCTestCase { } } } - """) + """) as! ObservableEventStream> var currentResult = GraphQLResult() - let _ = subscription.subscribe { event in + let _ = subscription.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -597,10 +597,10 @@ class SubscriptionTests : XCTestCase { } } } - """) + """) as! ObservableEventStream> var currentResult = GraphQLResult() - let subscriber = subscription.subscribe { event in + let subscriber = subscription.observable.subscribe { event in currentResult = try! event.element!.wait() } @@ -671,10 +671,10 @@ class SubscriptionTests : XCTestCase { } } } - """) + """) as! ObservableEventStream> var currentResult = GraphQLResult() - let _ = subscription.subscribe { event in + let _ = subscription.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -744,10 +744,10 @@ class SubscriptionTests : XCTestCase { } } } - """) + """) as! ObservableEventStream> var currentResult = GraphQLResult() - let _ = subscription.subscribe { event in + let _ = subscription.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -905,7 +905,7 @@ class EmailDb { func subscription ( query:String, variableValues: [String: Map] = [:] - ) throws -> SubscriptionObservable { + ) throws -> SubscriptionEventStream { return try createSubscription(schema: defaultSchema(), query: query, variableValues: variableValues) } } @@ -937,7 +937,7 @@ private func createSubscription( schema: GraphQLSchema, query: String, variableValues: [String: Map] = [:] -) throws -> SubscriptionObservable { +) throws -> SubscriptionEventStream { let document = try parse(source: query) let result = try subscribe( queryStrategy: SerialFieldExecutionStrategy(), @@ -953,8 +953,8 @@ private func createSubscription( operationName: nil ).wait() - if let observable = result.observable { - return observable + if let stream = result.stream { + return stream } else { throw result.errors.first! // We may have more than one... } From 33130f3cf260ec372838c3f5c006769c6178061a Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 10 Mar 2021 11:25:08 -0700 Subject: [PATCH 33/36] Adds convenience methods and passing tests --- Sources/GraphQL/GraphQL.swift | 1 - .../GraphQL/Subscription/EventStream.swift | 78 ++++++++++++-- Sources/GraphQL/Subscription/Subscribe.swift | 3 +- .../Subscription/SubscriptionTests.swift | 100 +++++++++++++----- 4 files changed, 139 insertions(+), 43 deletions(-) diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index e0a2ac83..c776e485 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -1,6 +1,5 @@ import Foundation import NIO -import RxSwift public struct GraphQLResult : Equatable, Codable, CustomStringConvertible { public var data: Map? diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift index 6166ae74..d79da3fb 100644 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ b/Sources/GraphQL/Subscription/EventStream.swift @@ -2,25 +2,81 @@ import RxSwift +/// Abstract event stream class - Should be overridden for actual implementations public class EventStream { - /// This class should be overridden - func transform(_ closure: @escaping (Element) throws -> To) -> EventStream { + /// Template method for mapping an event stream to a new generic type - MUST be overridden by implementing types. + func map(_ closure: @escaping (Element) throws -> To) -> EventStream { fatalError("This function should be overridden by implementing classes") } } -//extension Observable: EventStream { -// func transform(_ closure: @escaping (Element) throws -> To) -> EventStream { -// return self.map(closure) -// } -//} +// TODO: Put in separate GraphQLRxSwift package + +// EventStream wrapper for Observable public class ObservableEventStream : EventStream { - var observable: Observable - init(observable: Observable) { + public var observable: Observable + init(_ observable: Observable) { self.observable = observable } - override func transform(_ closure: @escaping (Element) throws -> To) -> EventStream { - return ObservableEventStream(observable: observable.map(closure)) + override func map(_ closure: @escaping (Element) throws -> To) -> EventStream { + return ObservableEventStream(observable.map(closure)) + } +} +// Convenience types +public typealias ObservableSourceEventStream = ObservableEventStream> +public typealias ObservableSubscriptionEventStream = ObservableEventStream> + +extension Observable { + // Convenience method for wrapping Observables in EventStreams + public func toEventStream() -> ObservableEventStream { + return ObservableEventStream(self) } } + + +// TODO: Delete notes below + +// Protocol attempts + +//protocol EventStreamP { +// associatedtype Element +// func transform(_ closure: @escaping (Element) throws -> To) -> EventStreamP // How to specify that returned associated type is 'To' +//} +//extension Observable: EventStreamP { +// func transform(_ closure: @escaping (Element) throws -> To) -> EventStreamP { +// return self.map(closure) +// } +//} + +// Try defining element in closure return +//protocol EventStreamP { +// associatedtype Element +// func transform(_ closure: @escaping (Element) throws -> ResultStream.Element) -> ResultStream +//} +//extension Observable: EventStreamP { +// func transform(_ closure: @escaping (Element) throws -> ResultStream.Element) -> ResultStream { +// return self.map(closure) // Observable isn't recognized as a ResultStream +// } +//} + +// Try absorbing generic type into function +//protocol EventStreamP { +// func transform(_ closure: @escaping (From) throws -> To) -> EventStreamP +//} +//extension Observable: EventStreamP { +// func transform(_ closure: @escaping (From) throws -> To) -> EventStreamP { +// return self.map(closure) // Doesn't recognize that Observable.Element is the same as From +// } +//} + +// Try opaque types +//protocol EventStreamP { +// associatedtype Element +// func transform(_ closure: @escaping (Element) throws -> To) -> some EventStreamP +//} +//extension Observable: EventStreamP { +// func transform(_ closure: @escaping (Element) throws -> To) -> some EventStreamP { +// return self.map(closure) +// } +//} diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 370dde42..0ada8d31 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -1,6 +1,5 @@ import Dispatch import Runtime -import RxSwift import NIO /** @@ -53,7 +52,7 @@ func subscribe( return sourceFuture.map{ sourceResult -> SubscriptionResult in if let sourceStream = sourceResult.stream { - let subscriptionStream = sourceStream.transform { eventPayload -> Future in + let subscriptionStream = sourceStream.map { eventPayload -> Future in // For each payload yielded from a subscription, map it over the normal // GraphQL `execute` function, with `payload` as the rootValue. diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index fc52a321..12c45038 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -32,11 +32,17 @@ class SubscriptionTests : XCTestCase { request: query, eventLoopGroup: eventLoopGroup ).wait() - print(subscriptionResult) - let subscription = subscriptionResult.stream! as! ObservableEventStream> + guard let subscription = subscriptionResult.stream else { + XCTFail(subscriptionResult.errors.description) + return + } + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } var currentResult = GraphQLResult() - let _ = subscription.observable.subscribe { event in + let _ = stream.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -86,7 +92,7 @@ class SubscriptionTests : XCTestCase { )) }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(db.publisher) + return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) } ), "notImportantEmail": GraphQLField( @@ -104,7 +110,7 @@ class SubscriptionTests : XCTestCase { )) }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(db.publisher) + return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) } ) ] @@ -123,10 +129,14 @@ class SubscriptionTests : XCTestCase { } } } - """) as! ObservableEventStream> + """) + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } var currentResult = GraphQLResult() - let _ = subscription.observable.subscribe { event in + let _ = stream.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -172,7 +182,7 @@ class SubscriptionTests : XCTestCase { }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in didResolveImportantEmail = true - return eventLoopGroup.next().makeSucceededFuture(db.publisher) + return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) } ), "notImportantEmail": GraphQLField( @@ -182,7 +192,7 @@ class SubscriptionTests : XCTestCase { }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in didResolveNonImportantEmail = true - return eventLoopGroup.next().makeSucceededFuture(db.publisher) + return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) } ) ] @@ -193,9 +203,13 @@ class SubscriptionTests : XCTestCase { importantEmail notImportantEmail } - """) as! ObservableEventStream> - - let _ = subscription.observable.subscribe{ event in + """) + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } + + let _ = stream.observable.subscribe{ event in let _ = try! event.element!.wait() }.disposed(by: db.disposeBag) db.trigger(email: Email( @@ -360,10 +374,14 @@ class SubscriptionTests : XCTestCase { } } } - """) as! ObservableEventStream> + """) + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } var currentResult = GraphQLResult() - let _ = subscription.observable.subscribe { event in + let _ = stream.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -403,17 +421,21 @@ class SubscriptionTests : XCTestCase { } } } - """) as! ObservableEventStream> + """) + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } // Subscription 1 var sub1Value = GraphQLResult() - let _ = subscription.observable.subscribe { event in + let _ = stream.observable.subscribe { event in sub1Value = try! event.element!.wait() }.disposed(by: db.disposeBag) // Subscription 2 var sub2Value = GraphQLResult() - let _ = subscription.observable.subscribe { event in + let _ = stream.observable.subscribe { event in sub2Value = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -457,10 +479,14 @@ class SubscriptionTests : XCTestCase { } } } - """) as! ObservableEventStream> + """) + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } var currentResult = GraphQLResult() - let _ = subscription.observable.subscribe { event in + let _ = stream.observable.subscribe { event in currentResult = try! event.element!.wait() print(currentResult) }.disposed(by: db.disposeBag) @@ -521,10 +547,14 @@ class SubscriptionTests : XCTestCase { } } } - """) as! ObservableEventStream> + """) + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } var currentResult = GraphQLResult() - let _ = subscription.observable.subscribe { event in + let _ = stream.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -597,10 +627,14 @@ class SubscriptionTests : XCTestCase { } } } - """) as! ObservableEventStream> + """) + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } var currentResult = GraphQLResult() - let subscriber = subscription.observable.subscribe { event in + let subscriber = stream.observable.subscribe { event in currentResult = try! event.element!.wait() } @@ -659,7 +693,7 @@ class SubscriptionTests : XCTestCase { )) }, subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(db.publisher) + return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) } ) @@ -671,10 +705,14 @@ class SubscriptionTests : XCTestCase { } } } - """) as! ObservableEventStream> + """) + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } var currentResult = GraphQLResult() - let _ = subscription.observable.subscribe { event in + let _ = stream.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -744,10 +782,14 @@ class SubscriptionTests : XCTestCase { } } } - """) as! ObservableEventStream> + """) + guard let stream = subscription as? ObservableSubscriptionEventStream else { + XCTFail("stream isn't ObservableSubscriptionEventStream") + return + } var currentResult = GraphQLResult() - let _ = subscription.observable.subscribe { event in + let _ = stream.observable.subscribe { event in currentResult = try! event.element!.wait() }.disposed(by: db.disposeBag) @@ -896,7 +938,7 @@ class EmailDb { return true } } - return eventLoopGroup.next().makeSucceededFuture(filtered) + return eventLoopGroup.next().makeSucceededFuture(filtered.toEventStream()) } ) } From 9da26da8b69d144a34303a2e62ccbb10cb2ee56f Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 10 Mar 2021 14:18:52 -0700 Subject: [PATCH 34/36] All subscription tests passing --- .../Subscription/SubscriptionSchema.swift | 200 ++++++++++++++++ .../Subscription/SubscriptionTests.swift | 224 ++---------------- 2 files changed, 221 insertions(+), 203 deletions(-) create mode 100644 Tests/GraphQLTests/Subscription/SubscriptionSchema.swift diff --git a/Tests/GraphQLTests/Subscription/SubscriptionSchema.swift b/Tests/GraphQLTests/Subscription/SubscriptionSchema.swift new file mode 100644 index 00000000..80820480 --- /dev/null +++ b/Tests/GraphQLTests/Subscription/SubscriptionSchema.swift @@ -0,0 +1,200 @@ +import GraphQL +import NIO +import RxSwift + +// MARK: Types +struct Email : Encodable { + let from:String + let subject:String + let message:String + let unread:Bool + let priority:Int + + init(from:String, subject:String, message:String, unread:Bool, priority:Int = 0) { + self.from = from + self.subject = subject + self.message = message + self.unread = unread + self.priority = priority + } +} + +struct Inbox : Encodable { + let emails:[Email] +} + +struct EmailEvent : Encodable { + let email:Email + let inbox:Inbox +} + +// MARK: Schema +let EmailType = try! GraphQLObjectType( + name: "Email", + fields: [ + "from": GraphQLField( + type: GraphQLString + ), + "subject": GraphQLField( + type: GraphQLString + ), + "message": GraphQLField( + type: GraphQLString + ), + "unread": GraphQLField( + type: GraphQLBoolean + ), + ] +) +let InboxType = try! GraphQLObjectType( + name: "Inbox", + fields: [ + "emails": GraphQLField( + type: GraphQLList(EmailType) + ), + "total": GraphQLField( + type: GraphQLInt, + resolve: { inbox, _, _, _ in + (inbox as! Inbox).emails.count + } + ), + "unread": GraphQLField( + type: GraphQLInt, + resolve: { inbox, _, _, _ in + (inbox as! Inbox).emails.filter({$0.unread}).count + } + ), + ] +) +let EmailEventType = try! GraphQLObjectType( + name: "EmailEvent", + fields: [ + "email": GraphQLField( + type: EmailType + ), + "inbox": GraphQLField( + type: InboxType + ) + ] +) +let EmailQueryType = try! GraphQLObjectType( + name: "Query", + fields: [ + "inbox": GraphQLField( + type: InboxType + ) + ] +) + +// MARK: Test Helpers + +let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + +class EmailDb { + var emails: [Email] + let publisher: PublishSubject + let disposeBag: DisposeBag + + init() { + emails = [ + Email( + from: "joe@graphql.org", + subject: "Hello", + message: "Hello World", + unread: false + ) + ] + publisher = PublishSubject() + disposeBag = DisposeBag() + } + + /// Adds a new email to the database and triggers all observers + func trigger(email:Email) { + emails.append(email) + publisher.onNext(email) + } + + /// Returns the default email schema, with standard resolvers. + func defaultSchema() -> GraphQLSchema { + return emailSchemaWithResolvers( + resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + if let email = emailAny as? Email { + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: self.emails) + )) + } else { + throw GraphQLError(message: "\(type(of:emailAny)) is not Email") + } + }, + subscribe: {_, args, _, eventLoopGroup, _ throws -> EventLoopFuture in + let priority = args["priority"].int ?? 0 + let filtered = self.publisher.filter { emailAny throws in + if let email = emailAny as? Email { + return email.priority >= priority + } else { + return true + } + } + return eventLoopGroup.next().makeSucceededFuture(filtered.toEventStream()) + } + ) + } + + /// Generates a subscription to the database using the default schema and resolvers + func subscription ( + query:String, + variableValues: [String: Map] = [:] + ) throws -> SubscriptionEventStream { + return try createSubscription(schema: defaultSchema(), query: query, variableValues: variableValues) + } +} + +/// Generates an email schema with the specified resolve and subscribe methods +func emailSchemaWithResolvers(resolve: GraphQLFieldResolve? = nil, subscribe: GraphQLFieldResolve? = nil) -> GraphQLSchema { + return try! GraphQLSchema( + query: EmailQueryType, + subscription: try! GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + args: [ + "priority": GraphQLArgument( + type: GraphQLInt + ) + ], + resolve: resolve, + subscribe: subscribe + ) + ] + ) + ) +} + +/// Generates a subscription from the given schema and query. It's expected that the resolver/database interactions are configured by the caller. +func createSubscription( + schema: GraphQLSchema, + query: String, + variableValues: [String: Map] = [:] +) throws -> SubscriptionEventStream { + let result = try graphqlSubscribe( + queryStrategy: SerialFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: SerialFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + schema: schema, + request: query, + rootValue: Void(), + context: Void(), + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: nil + ).wait() + + if let stream = result.stream { + return stream + } else { + throw result.errors.first! // We may have more than one... + } +} diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift index 12c45038..5f46875b 100644 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift @@ -200,8 +200,16 @@ class SubscriptionTests : XCTestCase { ) let subscription = try createSubscription(schema: schema, query: """ subscription { - importantEmail - notImportantEmail + importantEmail { + email { + from + } + } + notImportantEmail { + email { + from + } + } } """) guard let stream = subscription as? ObservableSubscriptionEventStream else { @@ -242,7 +250,7 @@ class SubscriptionTests : XCTestCase { ) ) { error in let graphQLError = error as! GraphQLError - XCTAssertEqual(graphQLError.message, "The subscription field 'unknownField' is not defined.") + XCTAssertEqual(graphQLError.message, "Cannot query field \"unknownField\" on type \"Subscription\".") XCTAssertEqual(graphQLError.locations, [SourceLocation(line: 2, column: 5)]) } } @@ -268,7 +276,11 @@ class SubscriptionTests : XCTestCase { XCTAssertThrowsError( try createSubscription(schema: schema, query: """ subscription { - importantEmail + importantEmail { + email { + from + } + } } """) ) { error in @@ -286,7 +298,11 @@ class SubscriptionTests : XCTestCase { XCTAssertThrowsError( try createSubscription(schema: schema, query: """ subscription { - importantEmail + importantEmail { + email { + from + } + } } """) ) { error in @@ -803,201 +819,3 @@ class SubscriptionTests : XCTestCase { )) } } - -// MARK: Types -struct Email : Encodable { - let from:String - let subject:String - let message:String - let unread:Bool - let priority:Int - - init(from:String, subject:String, message:String, unread:Bool, priority:Int = 0) { - self.from = from - self.subject = subject - self.message = message - self.unread = unread - self.priority = priority - } -} - -struct Inbox : Encodable { - let emails:[Email] -} - -struct EmailEvent : Encodable { - let email:Email - let inbox:Inbox -} - -// MARK: Schema -let EmailType = try! GraphQLObjectType( - name: "Email", - fields: [ - "from": GraphQLField( - type: GraphQLString - ), - "subject": GraphQLField( - type: GraphQLString - ), - "message": GraphQLField( - type: GraphQLString - ), - "unread": GraphQLField( - type: GraphQLBoolean - ), - ] -) -let InboxType = try! GraphQLObjectType( - name: "Inbox", - fields: [ - "emails": GraphQLField( - type: GraphQLList(EmailType) - ), - "total": GraphQLField( - type: GraphQLInt, - resolve: { inbox, _, _, _ in - (inbox as! Inbox).emails.count - } - ), - "unread": GraphQLField( - type: GraphQLInt, - resolve: { inbox, _, _, _ in - (inbox as! Inbox).emails.filter({$0.unread}).count - } - ), - ] -) -let EmailEventType = try! GraphQLObjectType( - name: "EmailEvent", - fields: [ - "email": GraphQLField( - type: EmailType - ), - "inbox": GraphQLField( - type: InboxType - ) - ] -) -let EmailQueryType = try! GraphQLObjectType( - name: "Query", - fields: [ - "inbox": GraphQLField( - type: InboxType - ) - ] -) - -// MARK: Test Helpers - -let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - -class EmailDb { - var emails: [Email] - let publisher: PublishSubject - let disposeBag: DisposeBag - - init() { - emails = [ - Email( - from: "joe@graphql.org", - subject: "Hello", - message: "Hello World", - unread: false - ) - ] - publisher = PublishSubject() - disposeBag = DisposeBag() - } - - /// Adds a new email to the database and triggers all observers - func trigger(email:Email) { - emails.append(email) - publisher.onNext(email) - } - - /// Returns the default email schema, with standard resolvers. - func defaultSchema() -> GraphQLSchema { - return emailSchemaWithResolvers( - resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - if let email = emailAny as? Email { - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: self.emails) - )) - } else { - throw GraphQLError(message: "\(type(of:emailAny)) is not Email") - } - }, - subscribe: {_, args, _, eventLoopGroup, _ throws -> EventLoopFuture in - let priority = args["priority"].int ?? 0 - let filtered = self.publisher.filter { emailAny throws in - if let email = emailAny as? Email { - return email.priority >= priority - } else { - return true - } - } - return eventLoopGroup.next().makeSucceededFuture(filtered.toEventStream()) - } - ) - } - - /// Generates a subscription to the database using the default schema and resolvers - func subscription ( - query:String, - variableValues: [String: Map] = [:] - ) throws -> SubscriptionEventStream { - return try createSubscription(schema: defaultSchema(), query: query, variableValues: variableValues) - } -} - -/// Generates an email schema with the specified resolve and subscribe methods -private func emailSchemaWithResolvers(resolve: GraphQLFieldResolve? = nil, subscribe: GraphQLFieldResolve? = nil) -> GraphQLSchema { - return try! GraphQLSchema( - query: EmailQueryType, - subscription: try! GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - args: [ - "priority": GraphQLArgument( - type: GraphQLInt - ) - ], - resolve: resolve, - subscribe: subscribe - ) - ] - ) - ) -} - -/// Generates a subscription from the given schema and query. It's expected that the resolver/database interactions are configured by the caller. -private func createSubscription( - schema: GraphQLSchema, - query: String, - variableValues: [String: Map] = [:] -) throws -> SubscriptionEventStream { - let document = try parse(source: query) - let result = try subscribe( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, - schema: schema, - documentAST: document, - rootValue: Void(), - context: Void(), - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: nil - ).wait() - - if let stream = result.stream { - return stream - } else { - throw result.errors.first! // We may have more than one... - } -} From e4beac690c243acf79073db27f776b2422372ea5 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 10 Mar 2021 14:27:51 -0700 Subject: [PATCH 35/36] Modifies EventStream permissions for package overrides --- Sources/GraphQL/Subscription/EventStream.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift index d79da3fb..7486f886 100644 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ b/Sources/GraphQL/Subscription/EventStream.swift @@ -3,9 +3,10 @@ import RxSwift /// Abstract event stream class - Should be overridden for actual implementations -public class EventStream { +open class EventStream { + public init() { } /// Template method for mapping an event stream to a new generic type - MUST be overridden by implementing types. - func map(_ closure: @escaping (Element) throws -> To) -> EventStream { + open func map(_ closure: @escaping (Element) throws -> To) -> EventStream { fatalError("This function should be overridden by implementing classes") } } @@ -19,7 +20,7 @@ public class ObservableEventStream : EventStream { init(_ observable: Observable) { self.observable = observable } - override func map(_ closure: @escaping (Element) throws -> To) -> EventStream { + override open func map(_ closure: @escaping (Element) throws -> To) -> EventStream { return ObservableEventStream(observable.map(closure)) } } From bee0675d07a243b2162d2a9c25297ba93b4188f5 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Wed, 10 Mar 2021 20:02:36 -0700 Subject: [PATCH 36/36] Moves all RxSwift dependency to GraphQLRxSwift --- Package.swift | 6 +- .../GraphQL/Subscription/EventStream.swift | 75 -- .../Subscription/SubscriptionSchema.swift | 200 ----- .../Subscription/SubscriptionTests.swift | 821 ------------------ 4 files changed, 2 insertions(+), 1100 deletions(-) delete mode 100644 Tests/GraphQLTests/Subscription/SubscriptionSchema.swift delete mode 100644 Tests/GraphQLTests/Subscription/SubscriptionTests.swift diff --git a/Package.swift b/Package.swift index fe37e802..311f7bf6 100644 --- a/Package.swift +++ b/Package.swift @@ -8,16 +8,14 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.10.1")), - .package(url: "https://github.com/wickwirew/Runtime.git", .upToNextMinor(from: "2.1.0")), - .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.1.0")) + .package(url: "https://github.com/wickwirew/Runtime.git", .upToNextMinor(from: "2.1.0")) ], targets: [ .target( name: "GraphQL", dependencies: [ .product(name: "NIO", package: "swift-nio"), - .product(name: "Runtime", package: "Runtime"), - .product(name: "RxSwift", package: "RxSwift") + .product(name: "Runtime", package: "Runtime") ] ), .testTarget(name: "GraphQLTests", dependencies: ["GraphQL"]), diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift index 7486f886..9288f506 100644 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ b/Sources/GraphQL/Subscription/EventStream.swift @@ -1,7 +1,3 @@ -// Copyright (c) 2021 PassiveLogic, Inc. - -import RxSwift - /// Abstract event stream class - Should be overridden for actual implementations open class EventStream { public init() { } @@ -10,74 +6,3 @@ open class EventStream { fatalError("This function should be overridden by implementing classes") } } - - -// TODO: Put in separate GraphQLRxSwift package - -// EventStream wrapper for Observable -public class ObservableEventStream : EventStream { - public var observable: Observable - init(_ observable: Observable) { - self.observable = observable - } - override open func map(_ closure: @escaping (Element) throws -> To) -> EventStream { - return ObservableEventStream(observable.map(closure)) - } -} -// Convenience types -public typealias ObservableSourceEventStream = ObservableEventStream> -public typealias ObservableSubscriptionEventStream = ObservableEventStream> - -extension Observable { - // Convenience method for wrapping Observables in EventStreams - public func toEventStream() -> ObservableEventStream { - return ObservableEventStream(self) - } -} - - -// TODO: Delete notes below - -// Protocol attempts - -//protocol EventStreamP { -// associatedtype Element -// func transform(_ closure: @escaping (Element) throws -> To) -> EventStreamP // How to specify that returned associated type is 'To' -//} -//extension Observable: EventStreamP { -// func transform(_ closure: @escaping (Element) throws -> To) -> EventStreamP { -// return self.map(closure) -// } -//} - -// Try defining element in closure return -//protocol EventStreamP { -// associatedtype Element -// func transform(_ closure: @escaping (Element) throws -> ResultStream.Element) -> ResultStream -//} -//extension Observable: EventStreamP { -// func transform(_ closure: @escaping (Element) throws -> ResultStream.Element) -> ResultStream { -// return self.map(closure) // Observable isn't recognized as a ResultStream -// } -//} - -// Try absorbing generic type into function -//protocol EventStreamP { -// func transform(_ closure: @escaping (From) throws -> To) -> EventStreamP -//} -//extension Observable: EventStreamP { -// func transform(_ closure: @escaping (From) throws -> To) -> EventStreamP { -// return self.map(closure) // Doesn't recognize that Observable.Element is the same as From -// } -//} - -// Try opaque types -//protocol EventStreamP { -// associatedtype Element -// func transform(_ closure: @escaping (Element) throws -> To) -> some EventStreamP -//} -//extension Observable: EventStreamP { -// func transform(_ closure: @escaping (Element) throws -> To) -> some EventStreamP { -// return self.map(closure) -// } -//} diff --git a/Tests/GraphQLTests/Subscription/SubscriptionSchema.swift b/Tests/GraphQLTests/Subscription/SubscriptionSchema.swift deleted file mode 100644 index 80820480..00000000 --- a/Tests/GraphQLTests/Subscription/SubscriptionSchema.swift +++ /dev/null @@ -1,200 +0,0 @@ -import GraphQL -import NIO -import RxSwift - -// MARK: Types -struct Email : Encodable { - let from:String - let subject:String - let message:String - let unread:Bool - let priority:Int - - init(from:String, subject:String, message:String, unread:Bool, priority:Int = 0) { - self.from = from - self.subject = subject - self.message = message - self.unread = unread - self.priority = priority - } -} - -struct Inbox : Encodable { - let emails:[Email] -} - -struct EmailEvent : Encodable { - let email:Email - let inbox:Inbox -} - -// MARK: Schema -let EmailType = try! GraphQLObjectType( - name: "Email", - fields: [ - "from": GraphQLField( - type: GraphQLString - ), - "subject": GraphQLField( - type: GraphQLString - ), - "message": GraphQLField( - type: GraphQLString - ), - "unread": GraphQLField( - type: GraphQLBoolean - ), - ] -) -let InboxType = try! GraphQLObjectType( - name: "Inbox", - fields: [ - "emails": GraphQLField( - type: GraphQLList(EmailType) - ), - "total": GraphQLField( - type: GraphQLInt, - resolve: { inbox, _, _, _ in - (inbox as! Inbox).emails.count - } - ), - "unread": GraphQLField( - type: GraphQLInt, - resolve: { inbox, _, _, _ in - (inbox as! Inbox).emails.filter({$0.unread}).count - } - ), - ] -) -let EmailEventType = try! GraphQLObjectType( - name: "EmailEvent", - fields: [ - "email": GraphQLField( - type: EmailType - ), - "inbox": GraphQLField( - type: InboxType - ) - ] -) -let EmailQueryType = try! GraphQLObjectType( - name: "Query", - fields: [ - "inbox": GraphQLField( - type: InboxType - ) - ] -) - -// MARK: Test Helpers - -let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - -class EmailDb { - var emails: [Email] - let publisher: PublishSubject - let disposeBag: DisposeBag - - init() { - emails = [ - Email( - from: "joe@graphql.org", - subject: "Hello", - message: "Hello World", - unread: false - ) - ] - publisher = PublishSubject() - disposeBag = DisposeBag() - } - - /// Adds a new email to the database and triggers all observers - func trigger(email:Email) { - emails.append(email) - publisher.onNext(email) - } - - /// Returns the default email schema, with standard resolvers. - func defaultSchema() -> GraphQLSchema { - return emailSchemaWithResolvers( - resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - if let email = emailAny as? Email { - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: self.emails) - )) - } else { - throw GraphQLError(message: "\(type(of:emailAny)) is not Email") - } - }, - subscribe: {_, args, _, eventLoopGroup, _ throws -> EventLoopFuture in - let priority = args["priority"].int ?? 0 - let filtered = self.publisher.filter { emailAny throws in - if let email = emailAny as? Email { - return email.priority >= priority - } else { - return true - } - } - return eventLoopGroup.next().makeSucceededFuture(filtered.toEventStream()) - } - ) - } - - /// Generates a subscription to the database using the default schema and resolvers - func subscription ( - query:String, - variableValues: [String: Map] = [:] - ) throws -> SubscriptionEventStream { - return try createSubscription(schema: defaultSchema(), query: query, variableValues: variableValues) - } -} - -/// Generates an email schema with the specified resolve and subscribe methods -func emailSchemaWithResolvers(resolve: GraphQLFieldResolve? = nil, subscribe: GraphQLFieldResolve? = nil) -> GraphQLSchema { - return try! GraphQLSchema( - query: EmailQueryType, - subscription: try! GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - args: [ - "priority": GraphQLArgument( - type: GraphQLInt - ) - ], - resolve: resolve, - subscribe: subscribe - ) - ] - ) - ) -} - -/// Generates a subscription from the given schema and query. It's expected that the resolver/database interactions are configured by the caller. -func createSubscription( - schema: GraphQLSchema, - query: String, - variableValues: [String: Map] = [:] -) throws -> SubscriptionEventStream { - let result = try graphqlSubscribe( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, - schema: schema, - request: query, - rootValue: Void(), - context: Void(), - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: nil - ).wait() - - if let stream = result.stream { - return stream - } else { - throw result.errors.first! // We may have more than one... - } -} diff --git a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift b/Tests/GraphQLTests/Subscription/SubscriptionTests.swift deleted file mode 100644 index 5f46875b..00000000 --- a/Tests/GraphQLTests/Subscription/SubscriptionTests.swift +++ /dev/null @@ -1,821 +0,0 @@ -import XCTest -import NIO -import RxSwift -@testable import GraphQL - -/// This follows the graphql-js testing, with deviations where noted. -class SubscriptionTests : XCTestCase { - - // MARK: Test primary graphqlSubscribe function - - /// This test is not present in graphql-js, but just tests basic functionality. - func testGraphqlSubscribe() throws { - let db = EmailDb() - let schema = db.defaultSchema() - let query = """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """ - - let subscriptionResult = try graphqlSubscribe( - schema: schema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() - guard let subscription = subscriptionResult.stream else { - XCTFail(subscriptionResult.errors.description) - return - } - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - var currentResult = GraphQLResult() - let _ = stream.observable.subscribe { event in - currentResult = try! event.element!.wait() - }.disposed(by: db.disposeBag) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 2, - "unread": 1 - ], - "email":[ - "subject": "Alright", - "from": "yuzhi@graphql.org" - ] - ]] - )) - } - - // MARK: Subscription Initialization Phase - - /// accepts multiple subscription fields defined in schema - func testAcceptsMultipleSubscriptionFields() throws { - let db = EmailDb() - let schema = try GraphQLSchema( - query: EmailQueryType, - subscription: try! GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - args: [ - "priority": GraphQLArgument( - type: GraphQLInt - ) - ], - resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - let email = emailAny as! Email - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: db.emails) - )) - }, - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) - } - ), - "notImportantEmail": GraphQLField( - type: EmailEventType, - args: [ - "priority": GraphQLArgument( - type: GraphQLInt - ) - ], - resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - let email = emailAny as! Email - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: db.emails) - )) - }, - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) - } - ) - ] - ) - ) - let subscription = try createSubscription(schema: schema, query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """) - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - var currentResult = GraphQLResult() - let _ = stream.observable.subscribe { event in - currentResult = try! event.element!.wait() - }.disposed(by: db.disposeBag) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 2, - "unread": 1 - ], - "email":[ - "subject": "Alright", - "from": "yuzhi@graphql.org" - ] - ]] - )) - } - - /// 'should only resolve the first field of invalid multi-field' - /// - /// Note that due to implementation details in Swift, this will not resolve the "first" one, but rather a random one of the two - func testInvalidMultiField() throws { - let db = EmailDb() - - var didResolveImportantEmail = false - var didResolveNonImportantEmail = false - - let schema = try GraphQLSchema( - query: EmailQueryType, - subscription: try! GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - resolve: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(nil) - }, - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - didResolveImportantEmail = true - return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) - } - ), - "notImportantEmail": GraphQLField( - type: EmailEventType, - resolve: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(nil) - }, - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - didResolveNonImportantEmail = true - return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) - } - ) - ] - ) - ) - let subscription = try createSubscription(schema: schema, query: """ - subscription { - importantEmail { - email { - from - } - } - notImportantEmail { - email { - from - } - } - } - """) - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - let _ = stream.observable.subscribe{ event in - let _ = try! event.element!.wait() - }.disposed(by: db.disposeBag) - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - - // One and only one should be true - XCTAssertTrue(didResolveImportantEmail || didResolveNonImportantEmail) - XCTAssertFalse(didResolveImportantEmail && didResolveNonImportantEmail) - } - - // 'throws an error if schema is missing' - // Not implemented because this is taken care of by Swift optional types - - // 'throws an error if document is missing' - // Not implemented because this is taken care of by Swift optional types - - /// 'resolves to an error for unknown subscription field' - func testErrorUnknownSubscriptionField() throws { - let db = EmailDb() - XCTAssertThrowsError( - try db.subscription(query: """ - subscription { - unknownField - } - """ - ) - ) { error in - let graphQLError = error as! GraphQLError - XCTAssertEqual(graphQLError.message, "Cannot query field \"unknownField\" on type \"Subscription\".") - XCTAssertEqual(graphQLError.locations, [SourceLocation(line: 2, column: 5)]) - } - } - - /// 'should pass through unexpected errors thrown in subscribe' - func testPassUnexpectedSubscribeErrors() throws { - let db = EmailDb() - XCTAssertThrowsError( - try db.subscription(query: "") - ) - } - - /// 'throws an error if subscribe does not return an iterator' - func testErrorIfSubscribeIsntIterator() throws { - let schema = emailSchemaWithResolvers( - resolve: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(nil) - }, - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture("test") - } - ) - XCTAssertThrowsError( - try createSubscription(schema: schema, query: """ - subscription { - importantEmail { - email { - from - } - } - } - """) - ) { error in - let graphQLError = error as! GraphQLError - XCTAssertEqual( - graphQLError.message, - "Subscription field resolver must return EventStream. Received: 'test'" - ) - } - } - - /// 'resolves to an error for subscription resolver errors' - func testErrorForSubscriptionResolverErrors() throws { - func verifyError(schema: GraphQLSchema) { - XCTAssertThrowsError( - try createSubscription(schema: schema, query: """ - subscription { - importantEmail { - email { - from - } - } - } - """) - ) { error in - let graphQLError = error as! GraphQLError - XCTAssertEqual(graphQLError.message, "test error") - } - } - - // Throwing an error - verifyError(schema: emailSchemaWithResolvers( - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - throw GraphQLError(message: "test error") - } - )) - - // Resolving to an error - verifyError(schema: emailSchemaWithResolvers( - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(GraphQLError(message: "test error")) - } - )) - - // Rejecting with an error - verifyError(schema: emailSchemaWithResolvers( - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeFailedFuture(GraphQLError(message: "test error")) - } - )) - } - - - /// 'resolves to an error for source event stream resolver errors' - // Tests above cover this - - /// 'resolves to an error if variables were wrong type' - func testErrorVariablesWrongType() throws { - let db = EmailDb() - let query = """ - subscription ($priority: Int) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """ - - XCTAssertThrowsError( - try db.subscription( - query: query, - variableValues: [ - "priority": "meow" - ] - ) - ) { error in - let graphQLError = error as! GraphQLError - XCTAssertEqual( - graphQLError.message, - "Variable \"$priority\" got invalid value \"meow\".\nExpected type \"Int\", found \"meow\"." - ) - } - } - - - // MARK: Subscription Publish Phase - - /// 'produces a payload for a single subscriber' - func testSingleSubscriber() throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """) - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - var currentResult = GraphQLResult() - let _ = stream.observable.subscribe { event in - currentResult = try! event.element!.wait() - }.disposed(by: db.disposeBag) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 2, - "unread": 1 - ], - "email":[ - "subject": "Alright", - "from": "yuzhi@graphql.org" - ] - ]] - )) - } - - /// 'produces a payload for multiple subscribe in same subscription' - func testMultipleSubscribers() throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """) - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - // Subscription 1 - var sub1Value = GraphQLResult() - let _ = stream.observable.subscribe { event in - sub1Value = try! event.element!.wait() - }.disposed(by: db.disposeBag) - - // Subscription 2 - var sub2Value = GraphQLResult() - let _ = stream.observable.subscribe { event in - sub2Value = try! event.element!.wait() - }.disposed(by: db.disposeBag) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - - let expected = GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 2, - "unread": 1 - ], - "email":[ - "subject": "Alright", - "from": "yuzhi@graphql.org" - ] - ]] - ) - - XCTAssertEqual(sub1Value, expected) - XCTAssertEqual(sub2Value, expected) - } - - /// 'produces a payload per subscription event' - func testPayloadPerEvent() throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """) - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - var currentResult = GraphQLResult() - let _ = stream.observable.subscribe { event in - currentResult = try! event.element!.wait() - print(currentResult) - }.disposed(by: db.disposeBag) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 2, - "unread": 1 - ], - "email":[ - "subject": "Alright", - "from": "yuzhi@graphql.org" - ] - ]] - )) - - db.trigger(email: Email( - from: "hyo@graphql.org", - subject: "Tools", - message: "I <3 making things", - unread: true - )) - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 3, - "unread": 2 - ], - "email":[ - "subject": "Tools", - "from": "hyo@graphql.org" - ] - ]] - )) - } - - /// Tests that subscriptions use arguments correctly. - /// This is not in the graphql-js tests. - func testArguments() throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 5) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """) - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - var currentResult = GraphQLResult() - let _ = stream.observable.subscribe { event in - currentResult = try! event.element!.wait() - }.disposed(by: db.disposeBag) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true, - priority: 7 - )) - let firstMessageExpected = GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 2, - "unread": 1 - ], - "email":[ - "subject": "Alright", - "from": "yuzhi@graphql.org" - ] - ]] - ) - XCTAssertEqual(currentResult, firstMessageExpected) - - // Low priority email shouldn't trigger an event - db.trigger(email: Email( - from: "hyo@graphql.org", - subject: "Not Important", - message: "Ignore this email", - unread: true, - priority: 2 - )) - XCTAssertEqual(currentResult, firstMessageExpected) - - // Higher priority one should trigger again - db.trigger(email: Email( - from: "hyo@graphql.org", - subject: "Tools", - message: "I <3 making things", - unread: true, - priority: 5 - )) - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 4, - "unread": 3 - ], - "email":[ - "subject": "Tools", - "from": "hyo@graphql.org" - ] - ]] - )) - } - - /// 'should not trigger when subscription is already done' - func testNoTriggerAfterDone() throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """) - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - var currentResult = GraphQLResult() - let subscriber = stream.observable.subscribe { event in - currentResult = try! event.element!.wait() - } - - let expected = GraphQLResult( - data: ["importantEmail": [ - "inbox":[ - "total": 2, - "unread": 1 - ], - "email":[ - "subject": "Alright", - "from": "yuzhi@graphql.org" - ] - ]] - ) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - XCTAssertEqual(currentResult, expected) - - subscriber.dispose() - - // This should not trigger an event. - db.trigger(email: Email( - from: "hyo@graphql.org", - subject: "Tools", - message: "I <3 making things", - unread: true - )) - XCTAssertEqual(currentResult, expected) - } - - /// 'should not trigger when subscription is thrown' - // Not necessary - Pub/sub implementation handles throwing/closing itself. - - /// 'event order is correct for multiple publishes' - // Not necessary - Pub/sub implementation handles event ordering - - /// 'should handle error during execution of source event' - func testErrorDuringSubscription() throws { - let db = EmailDb() - - let schema = emailSchemaWithResolvers( - resolve: {emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - let email = emailAny as! Email - if email.subject == "Goodbye" { // Force the system to fail here. - throw GraphQLError(message:"Never leave.") - } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: db.emails) - )) - }, - subscribe: {_, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - return eventLoopGroup.next().makeSucceededFuture(db.publisher.toEventStream()) - } - ) - - let subscription = try createSubscription(schema: schema, query: """ - subscription { - importantEmail { - email { - subject - } - } - } - """) - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - var currentResult = GraphQLResult() - let _ = stream.observable.subscribe { event in - currentResult = try! event.element!.wait() - }.disposed(by: db.disposeBag) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Hello", - message: "Tests are good", - unread: true - )) - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": [ - "email":[ - "subject": "Hello" - ] - ]] - )) - - // An error in execution is presented as such. - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Goodbye", - message: "Tests are good", - unread: true - )) - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": nil], - errors: [ - GraphQLError(message: "Never leave.") - ] - )) - - // However that does not close the response event stream. Subsequent events are still executed. - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Bonjour", - message: "Tests are good", - unread: true - )) - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": [ - "email":[ - "subject": "Bonjour" - ] - ]] - )) - } - - /// 'should pass through error thrown in source event stream' - // Not necessary - Pub/sub implementation handles event erroring - - /// 'should resolve GraphQL error from source event stream' - // Not necessary - Pub/sub implementation handles event erroring - - /// Test incorrect observable publish type errors - func testErrorWrongObservableType() throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """) - guard let stream = subscription as? ObservableSubscriptionEventStream else { - XCTFail("stream isn't ObservableSubscriptionEventStream") - return - } - - var currentResult = GraphQLResult() - let _ = stream.observable.subscribe { event in - currentResult = try! event.element!.wait() - }.disposed(by: db.disposeBag) - - db.publisher.onNext("String instead of email") - - XCTAssertEqual(currentResult, GraphQLResult( - data: ["importantEmail": nil], - errors: [ - GraphQLError(message: "String is not Email") - ] - )) - } -}