From 24080b6fdb5885afd7c4a1f30e2e0b6458d72dbe Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 23 Jun 2025 16:54:49 -0600 Subject: [PATCH 1/8] feat!: Uses swift concurrency under the hood This removes the NIO dependency. It is breaking because it removes all Swift NIO-isms that were present in the public APIs (like EventLoopFuture and EventLoopGroup argument/return types). --- MIGRATION.md | 20 +- Package.resolved | 27 - Package.swift | 3 +- README.md | 13 +- Sources/GraphQL/Execution/Execute.swift | 423 +++---- Sources/GraphQL/GraphQL.swift | 161 +-- .../DispatchQueueInstrumentationWrapper.swift | 7 +- .../Instrumentation/Instrumentation.swift | 9 +- .../GraphQL/Subscription/EventStream.swift | 8 +- Sources/GraphQL/Subscription/Subscribe.swift | 116 +- Sources/GraphQL/Type/Definition.swift | 10 +- Sources/GraphQL/Type/Introspection.swift | 13 +- .../GraphQL/Utilities/NIO+Extensions.swift | 98 -- .../ExecutionTests/OneOfTests.swift | 51 +- .../FieldExecutionStrategyTests.swift | 121 +- .../HelloWorldTests/HelloWorldTests.swift | 40 +- .../GraphQLTests/InputTests/InputTests.swift | 1116 ++++++++--------- .../InstrumentationTests.swift | 5 +- .../StarWarsIntrospectionTests.swift | 127 +- .../StarWarsTests/StarWarsQueryTests.swift | 260 ++-- .../StarWarsTests/StarWarsSchema.swift | 6 +- .../SubscriptionSchema.swift | 25 +- .../SubscriptionTests/SubscriptionTests.swift | 203 ++- .../TypeTests/GraphQLSchemaTests.swift | 6 +- .../TypeTests/IntrospectionTests.swift | 20 +- .../GraphQLTests/TypeTests/ScalarTests.swift | 1 - .../UtilitiesTests/BuildASTSchemaTests.swift | 17 +- .../UtilitiesTests/ExtendSchemaTests.swift | 20 +- .../ValidationTests/ExampleSchema.swift | 18 +- 29 files changed, 1121 insertions(+), 1823 deletions(-) delete mode 100644 Sources/GraphQL/Utilities/NIO+Extensions.swift diff --git a/MIGRATION.md b/MIGRATION.md index b5c3d3ce..8ef81076 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,22 @@ # Migration -## 2.0 to 3.0 +## 3 to 4 + +### NIO removal + +All NIO-based arguments and return types were removed, including all `EventLoopGroup` and `EventLoopFuture` parameters. + +As such, all `execute` and `subscribe` calls should have the `eventLoopGroup` argument removed, and the `await` keyword should be used. + +Also, all resolver closures must remove the `eventLoopGroup` argument, and all that return an `EventLoopFuture` should be converted to an `async` function. + +The documentation here will be very helpful in the conversion: https://www.swift.org/documentation/server/guides/libraries/concurrency-adoption-guidelines.html + +### `ConcurrentDispatchFieldExecutionStrategy` + +This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. + +## 2 to 3 ### TypeReference removal @@ -73,4 +89,4 @@ The following type properties were changed from arrays to closures. To get the a ### GraphQL type codability -With GraphQL type definitions now including closures, many of the objects in [Definition](https://github.com/GraphQLSwift/GraphQL/blob/main/Sources/GraphQL/Type/Definition.swift) are no longer codable. If you are depending on codability, you can conform the type appropriately in your downstream package. \ No newline at end of file +With GraphQL type definitions now including closures, many of the objects in [Definition](https://github.com/GraphQLSwift/GraphQL/blob/main/Sources/GraphQL/Type/Definition.swift) are no longer codable. If you are depending on codability, you can conform the type appropriately in your downstream package. diff --git a/Package.resolved b/Package.resolved index d4fd520f..a9ec17d4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics.git", - "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" - } - }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", @@ -17,24 +8,6 @@ "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", "version" : "1.1.4" } - }, - { - "identity" : "swift-nio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", - "state" : { - "revision" : "27c839f4700069928196cd0e9fa03b22f297078a", - "version" : "2.78.0" - } - }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system.git", - "state" : { - "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", - "version" : "1.4.0" - } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 2838065f..9bf74c2f 100644 --- a/Package.swift +++ b/Package.swift @@ -3,18 +3,17 @@ import PackageDescription let package = Package( name: "GraphQL", + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ .library(name: "GraphQL", targets: ["GraphQL"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.10.1")), .package(url: "https://github.com/apple/swift-collections", .upToNextMajor(from: "1.0.0")), ], targets: [ .target( name: "GraphQL", dependencies: [ - .product(name: "NIO", package: "swift-nio"), .product(name: "OrderedCollections", package: "swift-collections"), ] ), diff --git a/README.md b/README.md index 87aeef25..06b7ae2f 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,7 @@ Once a schema has been defined queries may be executed against it using the glob ```swift let result = try await graphql( schema: schema, - request: "{ hello }", - eventLoopGroup: eventLoopGroup + request: "{ hello }" ) ``` @@ -59,7 +58,7 @@ The result of this query is a `GraphQLResult` that encodes to the following JSON ### Subscription This package supports GraphQL subscription, but until the integration of `AsyncSequence` in Swift 5.5 the standard Swift library did not -provide an event-stream construct. For historical reasons and backwards compatibility, this library implements subscriptions using an +provide an event-stream construct. For historical reasons and backwards compatibility, this library implements subscriptions using an `EventStream` protocol that nearly every asynchronous stream implementation can conform to. To create a subscription field in a GraphQL schema, use the `subscribe` resolver that returns an `EventStream`. You must also provide a @@ -70,7 +69,7 @@ let schema = try GraphQLSchema( subscribe: GraphQLObjectType( name: "Subscribe", fields: [ - "hello": GraphQLField( + "hello": GraphQLField( type: GraphQLString, resolve: { eventResult, _, _, _, _ in // Defines how to transform each event when it occurs return eventResult @@ -116,13 +115,13 @@ The example above assumes that your environment has access to Swift Concurrency. ## Encoding Results -If you encode a `GraphQLResult` with an ordinary `JSONEncoder`, there are no guarantees that the field order will match the query, +If you encode a `GraphQLResult` with an ordinary `JSONEncoder`, there are no guarantees that the field order will match the query, violating the [GraphQL spec](https://spec.graphql.org/June2018/#sec-Serialized-Map-Ordering). To preserve this order, `GraphQLResult` should be encoded using the `GraphQLJSONEncoder` provided by this package. ## Support -This package supports Swift versions in [alignment with Swift NIO](https://github.com/apple/swift-nio?tab=readme-ov-file#swift-versions). +This package aims to support the previous three Swift versions. For details on upgrading to new major versions, see [MIGRATION](MIGRATION.md). @@ -140,7 +139,7 @@ To format your code, install `swiftformat` and run: ```bash swiftformat . -``` +``` Most of this repo mirrors the structure of (the canonical GraphQL implementation written in Javascript/Typescript)[https://github.com/graphql/graphql-js]. If there is any feature diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index f082b721..005f3359 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -1,5 +1,4 @@ import Dispatch -import NIO import OrderedCollections /** @@ -37,7 +36,6 @@ public final class ExecutionContext { public let fragments: [String: FragmentDefinition] public let rootValue: Any public let context: Any - public let eventLoopGroup: EventLoopGroup public let operation: OperationDefinition public let variableValues: [String: Map] @@ -61,7 +59,6 @@ public final class ExecutionContext { fragments: [String: FragmentDefinition], rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, operation: OperationDefinition, variableValues: [String: Map], errors: [GraphQLError] @@ -74,7 +71,6 @@ public final class ExecutionContext { self.fragments = fragments self.rootValue = rootValue self.context = context - self.eventLoopGroup = eventLoopGroup self.operation = operation self.variableValues = variableValues _errors = errors @@ -96,7 +92,7 @@ public protocol FieldExecutionStrategy { sourceValue: Any, path: IndexPath, fields: OrderedDictionary - ) throws -> Future> + ) async throws -> OrderedDictionary } public protocol MutationFieldExecutionStrategy: FieldExecutionStrategy {} @@ -117,30 +113,20 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, sourceValue: Any, path: IndexPath, fields: OrderedDictionary - ) throws -> Future> { + ) async throws -> OrderedDictionary { var results = OrderedDictionary() - - return fields - .reduce(exeContext.eventLoopGroup.next().makeSucceededVoidFuture()) { prev, field in - // We use ``flatSubmit`` here to avoid a stack overflow issue with EventLoopFutures. - // See: https://github.com/apple/swift-nio/issues/970 - exeContext.eventLoopGroup.next().flatSubmit { - prev.tryFlatMap { - let fieldASTs = field.value - let fieldPath = path.appending(field.key) - - return try resolveField( - exeContext: exeContext, - parentType: parentType, - source: sourceValue, - fieldASTs: fieldASTs, - path: fieldPath - ).map { result in - results[field.key] = result ?? Map.null - } - } - } - }.map { results } + for field in fields { + let fieldASTs = field.value + let fieldPath = path.appending(field.key) + results[field.key] = try await resolveField( + exeContext: exeContext, + parentType: parentType, + source: sourceValue, + fieldASTs: fieldASTs, + path: fieldPath + ) ?? Map.null + } + return results } } @@ -149,78 +135,38 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, * * Each field is resolved as an individual task on a concurrent dispatch queue. */ -public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrategy, +public struct ConcurrentFieldExecutionStrategy: QueryFieldExecutionStrategy, SubscriptionFieldExecutionStrategy { - let dispatchQueue: DispatchQueue - - public init(dispatchQueue: DispatchQueue) { - self.dispatchQueue = dispatchQueue - } - - public init( - queueLabel: String = "GraphQL field execution", - queueQoS: DispatchQoS = .userInitiated - ) { - dispatchQueue = DispatchQueue( - label: queueLabel, - qos: queueQoS, - attributes: .concurrent - ) - } - public func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, sourceValue: Any, path: IndexPath, fields: OrderedDictionary - ) throws -> Future> { - let resultsQueue = DispatchQueue( - label: "\(dispatchQueue.label) results", - qos: dispatchQueue.qos - ) - - let group = DispatchGroup() - // preserve field order by assigning to null and filtering later - var results: OrderedDictionary?> = fields - .mapValues { _ -> Future? in nil } - var err: Error? - - for field in fields { - let fieldASTs = field.value - let fieldKey = field.key - let fieldPath = path.appending(fieldKey) - dispatchQueue.async(group: group) { - guard err == nil else { - return - } - do { - let result = try resolveField( + ) async throws -> OrderedDictionary { + return try await withThrowingTaskGroup(of: (String, Any?).self) { group in + // preserve field order by assigning to null and filtering later + var results: OrderedDictionary = fields.mapValues { _ -> Any? in nil } + for field in fields { + group.addTask { + let fieldASTs = field.value + let fieldPath = path.appending(field.key) + let result = try await resolveField( exeContext: exeContext, parentType: parentType, source: sourceValue, fieldASTs: fieldASTs, path: fieldPath - ) - resultsQueue.async(group: group) { - results[fieldKey] = result.map { $0 ?? Map.null } - } - } catch { - resultsQueue.async(group: group) { - err = error - } + ) ?? Map.null + return (field.key, result) } } + for try await result in group { + results[result.0] = result.1 + } + return results.compactMapValues { $0 } } - - group.wait() - - if let error = err { - throw error - } - - return results.compactMapValues { $0 }.flatten(on: exeContext.eventLoopGroup) } } @@ -239,10 +185,9 @@ func execute( documentAST: Document, rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) -> Future { +) async throws -> GraphQLResult { let executeStarted = instrumentation.now let buildContext: ExecutionContext @@ -258,7 +203,6 @@ func execute( documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, rawVariableValues: variableValues, operationName: operationName ) @@ -271,72 +215,42 @@ func execute( schema: schema, document: documentAST, rootValue: rootValue, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operation: nil, errors: [error], result: nil ) - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [error])) + return GraphQLResult(errors: [error]) } catch { - return eventLoopGroup.next() - .makeSucceededFuture(GraphQLResult(errors: [GraphQLError(error)])) + return GraphQLResult(errors: [GraphQLError(error)]) } do { // var executeErrors: [GraphQLError] = [] - - return try executeOperation( + let data = try await executeOperation( exeContext: buildContext, operation: buildContext.operation, rootValue: rootValue - ).flatMapThrowing { data -> GraphQLResult in - var dataMap: Map = [:] + ) + var dataMap: Map = [:] - for (key, value) in data { - dataMap[key] = try map(from: value) - } + for (key, value) in data { + dataMap[key] = try map(from: value) + } - var result: GraphQLResult = .init(data: dataMap) + var result: GraphQLResult = .init(data: dataMap) - if !buildContext.errors.isEmpty { - result.errors = buildContext.errors - } + if !buildContext.errors.isEmpty { + result.errors = buildContext.errors + } // executeErrors = buildContext.errors - return result - }.flatMapError { error -> Future in - let result: GraphQLResult - if let error = error as? GraphQLError { - result = GraphQLResult(errors: [error]) - } else { - result = GraphQLResult(errors: [GraphQLError(error)]) - } - - return buildContext.eventLoopGroup.next().makeSucceededFuture(result) - }.map { result -> GraphQLResult in -// instrumentation.operationExecution( -// processId: processId(), -// threadId: threadId(), -// started: executeStarted, -// finished: instrumentation.now, -// schema: schema, -// document: documentAST, -// rootValue: rootValue, -// eventLoopGroup: eventLoopGroup, -// variableValues: variableValues, -// operation: buildContext.operation, -// errors: executeErrors, -// result: result -// ) - result - } + return result } catch let error as GraphQLError { - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [error])) + return GraphQLResult(errors: [error]) } catch { - return eventLoopGroup.next() - .makeSucceededFuture(GraphQLResult(errors: [GraphQLError(error)])) + return GraphQLResult(errors: [GraphQLError(error)]) } } @@ -355,7 +269,6 @@ func buildExecutionContext( documentAST: Document, rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, rawVariableValues: [String: Map], operationName: String? ) throws -> ExecutionContext { @@ -410,7 +323,6 @@ func buildExecutionContext( fragments: fragments, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, operation: operation, variableValues: variableValues, errors: errors @@ -424,7 +336,7 @@ func executeOperation( exeContext: ExecutionContext, operation: OperationDefinition, rootValue: Any -) throws -> Future> { +) async throws -> OrderedDictionary { let type = try getOperationRootType(schema: exeContext.schema, operation: operation) var inputFields: OrderedDictionary = [:] var visitedFragmentNames: [String: Bool] = [:] @@ -448,7 +360,7 @@ func executeOperation( fieldExecutionStrategy = exeContext.subscriptionStrategy } - return try fieldExecutionStrategy.executeFields( + return try await fieldExecutionStrategy.executeFields( exeContext: exeContext, parentType: type, sourceValue: rootValue, @@ -687,7 +599,7 @@ public func resolveField( source: Any, fieldASTs: [Field], path: IndexPath -) throws -> Future { +) async throws -> Any? { let fieldAST = fieldASTs[0] let fieldName = fieldAST.name.value @@ -733,12 +645,11 @@ public func resolveField( // Get the resolve func, regardless of if its result is normal // or abrupt (error). - let result = resolveOrError( + let result = await resolveOrError( resolve: resolve, source: source, args: args, context: context, - eventLoopGroup: exeContext.eventLoopGroup, info: info ) @@ -749,12 +660,11 @@ public func resolveField( // finished: exeContext.instrumentation.now, // source: source, // args: args, -// eventLoopGroup: exeContext.eventLoopGroup, // info: info, // result: result // ) - return try completeValueCatchingError( + return try await completeValueCatchingError( exeContext: exeContext, returnType: returnType, fieldASTs: fieldASTs, @@ -771,11 +681,10 @@ func resolveOrError( source: Any, args: Map, context: Any, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo -) -> Result, Error> { +) async -> Result { do { - let result = try resolve(source, args, context, eventLoopGroup, info) + let result = try await resolve(source, args, context, info) return .success(result) } catch { return .failure(error) @@ -790,12 +699,12 @@ func completeValueCatchingError( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result, Error> -) throws -> Future { + result: Result +) async throws -> Any? { // If the field type is non-nullable, then it is resolved without any // protection from errors, however it still properly locates the error. if let returnType = returnType as? GraphQLNonNull { - return try completeValueWithLocatedError( + return try await completeValueWithLocatedError( exeContext: exeContext, returnType: returnType, fieldASTs: fieldASTs, @@ -808,29 +717,21 @@ func completeValueCatchingError( // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. do { - let completed = try completeValueWithLocatedError( + return try await completeValueWithLocatedError( exeContext: exeContext, returnType: returnType, fieldASTs: fieldASTs, info: info, path: path, result: result - ).flatMapError { error -> EventLoopFuture in - guard let error = error as? GraphQLError else { - return exeContext.eventLoopGroup.next().makeFailedFuture(error) - } - exeContext.append(error: error) - return exeContext.eventLoopGroup.next().makeSucceededFuture(nil) - } - - return completed + ) } catch let error as GraphQLError { // If `completeValueWithLocatedError` returned abruptly (threw an error), // log the error and return .null. exeContext.append(error: error) - return exeContext.eventLoopGroup.next().makeSucceededFuture(nil) + return nil } catch { - return exeContext.eventLoopGroup.next().makeFailedFuture(error) + throw error } } @@ -842,20 +743,17 @@ func completeValueWithLocatedError( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result, Error> -) throws -> Future { + result: Result +) async throws -> Any? { do { - let completed = try completeValue( + return try await completeValue( exeContext: exeContext, returnType: returnType, fieldASTs: fieldASTs, info: info, path: path, result: result - ).flatMapErrorThrowing { error -> Any? in - throw locatedError(originalError: error, nodes: fieldASTs, path: path) - } - return completed + ) } catch { throw locatedError( originalError: error, @@ -892,8 +790,8 @@ func completeValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result, Error> -) throws -> Future { + result: Result +) async throws -> Any? { switch result { case let .failure(error): throw error @@ -901,79 +799,75 @@ func completeValue( // If field type is NonNull, complete for inner type, and throw field error // if result is nullish. if let returnType = returnType as? GraphQLNonNull { - return try completeValue( + let value = try await completeValue( exeContext: exeContext, returnType: returnType.ofType, fieldASTs: fieldASTs, info: info, path: path, result: .success(result) - ).flatMapThrowing { value -> Any? in - guard let value = value else { - throw GraphQLError( - message: "Cannot return null for non-nullable field \(info.parentType.name).\(info.fieldName)." - ) - } - - return value + ) + guard let value = value else { + throw GraphQLError( + message: "Cannot return null for non-nullable field \(info.parentType.name).\(info.fieldName)." + ) } - } - return result.tryFlatMap { result throws -> Future in - // If result value is null-ish (nil or .null) then return .null. - guard let result = result, let r = unwrap(result) else { - return exeContext.eventLoopGroup.next().makeSucceededFuture(nil) - } + return value + } - // If field type is List, complete each item in the list with the inner type - if let returnType = returnType as? GraphQLList { - return try completeListValue( - exeContext: exeContext, - returnType: returnType, - fieldASTs: fieldASTs, - info: info, - path: path, - result: r - ).map { $0 } - } + // If result value is null-ish (nil or .null) then return .null. + guard let result = result, let r = unwrap(result) else { + return nil + } - // If field type is a leaf type, Scalar or Enum, serialize to a valid value, - // returning .null if serialization is not possible. - if let returnType = returnType as? GraphQLLeafType { - return try exeContext.eventLoopGroup.next() - .makeSucceededFuture(completeLeafValue(returnType: returnType, result: r)) - } + // If field type is List, complete each item in the list with the inner type + if let returnType = returnType as? GraphQLList { + return try await completeListValue( + exeContext: exeContext, + returnType: returnType, + fieldASTs: fieldASTs, + info: info, + path: path, + result: r + ) + } - // If field type is an abstract type, Interface or Union, determine the - // runtime Object type and complete for that type. - if let returnType = returnType as? GraphQLAbstractType { - return try completeAbstractValue( - exeContext: exeContext, - returnType: returnType, - fieldASTs: fieldASTs, - info: info, - path: path, - result: r - ) - } + // If field type is a leaf type, Scalar or Enum, serialize to a valid value, + // returning .null if serialization is not possible. + if let returnType = returnType as? GraphQLLeafType { + return try completeLeafValue(returnType: returnType, result: r) + } - // If field type is Object, execute and complete all sub-selections. - if let returnType = returnType as? GraphQLObjectType { - return try completeObjectValue( - exeContext: exeContext, - returnType: returnType, - fieldASTs: fieldASTs, - info: info, - path: path, - result: r - ) - } + // If field type is an abstract type, Interface or Union, determine the + // runtime Object type and complete for that type. + if let returnType = returnType as? GraphQLAbstractType { + return try await completeAbstractValue( + exeContext: exeContext, + returnType: returnType, + fieldASTs: fieldASTs, + info: info, + path: path, + result: r + ) + } - // Not reachable. All possible output types have been considered. - throw GraphQLError( - message: "Cannot complete value of unexpected type \"\(returnType)\"." + // If field type is Object, execute and complete all sub-selections. + if let returnType = returnType as? GraphQLObjectType { + return try await completeObjectValue( + exeContext: exeContext, + returnType: returnType, + fieldASTs: fieldASTs, + info: info, + path: path, + result: r ) } + + // Not reachable. All possible output types have been considered. + throw GraphQLError( + message: "Cannot complete value of unexpected type \"\(returnType)\"." + ) } } @@ -988,7 +882,7 @@ func completeListValue( info: GraphQLResolveInfo, path: IndexPath, result: Any -) throws -> Future<[Any?]> { +) async throws -> [Any?] { guard let result = result as? [Any?] else { throw GraphQLError( message: @@ -998,28 +892,32 @@ func completeListValue( } let itemType = returnType.ofType - var completedResults: [Future] = [] - for (index, item) in result.enumerated() { - // No need to modify the info object containing the path, - // since from here on it is not ever accessed by resolver funcs. - let fieldPath = path.appending(index) - let futureItem = item as? Future ?? exeContext.eventLoopGroup.next() - .makeSucceededFuture(item) + return try await withThrowingTaskGroup(of: (Int, Any?).self) { group in + // To preserve order, match size to result, and filter out nils at the end. + var results: [Any?] = result.map { _ in nil } + for (index, item) in result.enumerated() { + group.addTask { + // No need to modify the info object containing the path, + // since from here on it is not ever accessed by resolver funcs. + let fieldPath = path.appending(index) - let completedItem = try completeValueCatchingError( - exeContext: exeContext, - returnType: itemType, - fieldASTs: fieldASTs, - info: info, - path: fieldPath, - result: .success(futureItem) - ) - - completedResults.append(completedItem) + let result = try await completeValueCatchingError( + exeContext: exeContext, + returnType: itemType, + fieldASTs: fieldASTs, + info: info, + path: fieldPath, + result: .success(item) + ) + return (index, result) + } + for try await result in group { + results[result.0] = result.1 + } + } + return results.compactMap { $0 } } - - return completedResults.flatten(on: exeContext.eventLoopGroup) } /** @@ -1048,13 +946,12 @@ func completeAbstractValue( info: GraphQLResolveInfo, path: IndexPath, result: Any -) throws -> Future { - var resolveRes = try returnType.resolveType?(result, exeContext.eventLoopGroup, info) +) async throws -> Any? { + var resolveRes = try returnType.resolveType?(result, info) .typeResolveResult resolveRes = try resolveRes ?? defaultResolveType( value: result, - eventLoopGroup: exeContext.eventLoopGroup, info: info, abstractType: returnType ) @@ -1095,7 +992,7 @@ func completeAbstractValue( ) } - return try completeObjectValue( + return try await completeObjectValue( exeContext: exeContext, returnType: objectType, fieldASTs: fieldASTs, @@ -1115,13 +1012,13 @@ func completeObjectValue( info: GraphQLResolveInfo, path: IndexPath, result: Any -) throws -> Future { +) async throws -> Any? { // If there is an isTypeOf predicate func, call it with the // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. if let isTypeOf = returnType.isTypeOf, - try !isTypeOf(result, exeContext.eventLoopGroup, info) + try !isTypeOf(result, info) { throw GraphQLError( message: @@ -1146,13 +1043,13 @@ func completeObjectValue( } } - return try exeContext.queryStrategy.executeFields( + return try await exeContext.queryStrategy.executeFields( exeContext: exeContext, parentType: returnType, sourceValue: result, path: path, fields: subFieldASTs - ).map { $0 } + ) } /** @@ -1162,7 +1059,6 @@ func completeObjectValue( */ func defaultResolveType( value: Any, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo, abstractType: GraphQLAbstractType ) throws -> TypeResolveResult? { @@ -1170,7 +1066,7 @@ func defaultResolveType( guard let type = try possibleTypes - .find({ try $0.isTypeOf?(value, eventLoopGroup, info) ?? false }) + .find({ try $0.isTypeOf?(value, info) ?? false }) else { return nil } @@ -1187,31 +1083,30 @@ func defaultResolve( source: Any, args _: Map, context _: Any, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo -) -> Future { +) async throws -> Any? { guard let source = unwrap(source) else { - return eventLoopGroup.next().makeSucceededFuture(nil) + return nil } if let subscriptable = source as? KeySubscriptable { let value = subscriptable[info.fieldName] - return eventLoopGroup.next().makeSucceededFuture(value) + return value } if let subscriptable = source as? [String: Any] { let value = subscriptable[info.fieldName] - return eventLoopGroup.next().makeSucceededFuture(value) + return value } if let subscriptable = source as? OrderedDictionary { let value = subscriptable[info.fieldName] - return eventLoopGroup.next().makeSucceededFuture(value) + return value } let mirror = Mirror(reflecting: source) guard let value = mirror.getValue(named: info.fieldName) else { - return eventLoopGroup.next().makeSucceededFuture(nil) + return nil } - return eventLoopGroup.next().makeSucceededFuture(value) + return value } /** diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 0cc6c643..0488bb44 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -1,4 +1,3 @@ -import NIO public struct GraphQLResult: Equatable, Codable, Sendable, CustomStringConvertible { public var data: Map? @@ -56,7 +55,7 @@ public struct SubscriptionResult { /// SubscriptionObservable represents an event stream of fully resolved GraphQL subscription /// results. Subscribers can be added to this stream. -public typealias SubscriptionEventStream = EventStream> +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 @@ -101,10 +100,9 @@ public func graphql( request: String, rootValue: Any = (), context: Any = (), - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) throws -> Future { +) async throws -> GraphQLResult { let source = Source(body: request, name: "GraphQL request") let documentAST = try parse(instrumentation: instrumentation, source: source) let validationErrors = validate( @@ -115,10 +113,10 @@ public func graphql( ) guard validationErrors.isEmpty else { - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: validationErrors)) + return GraphQLResult(errors: validationErrors) } - return execute( + return try await execute( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -127,7 +125,6 @@ public func graphql( documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) @@ -169,19 +166,18 @@ public func graphql( queryId: Retrieval.Id, rootValue: Any = (), context: Any = (), - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) throws -> Future { +) async throws -> GraphQLResult { switch try queryRetrieval.lookup(queryId) { case .unknownId: throw GraphQLError(message: "Unknown query id") case let .parseError(parseError): throw parseError case let .validateErrors(_, validationErrors): - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: validationErrors)) + return GraphQLResult(errors: validationErrors) case let .result(schema, documentAST): - return execute( + return try await execute( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -190,7 +186,6 @@ public func graphql( documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) @@ -244,10 +239,9 @@ public func graphqlSubscribe( request: String, rootValue: Any = (), context: Any = (), - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) throws -> Future { +) async throws -> SubscriptionResult { let source = Source(body: request, name: "GraphQL Subscription request") let documentAST = try parse(instrumentation: instrumentation, source: source) let validationErrors = validate( @@ -258,11 +252,10 @@ public func graphqlSubscribe( ) guard validationErrors.isEmpty else { - return eventLoopGroup.next() - .makeSucceededFuture(SubscriptionResult(errors: validationErrors)) + return SubscriptionResult(errors: validationErrors) } - return subscribe( + return try await subscribe( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -271,141 +264,7 @@ public func graphqlSubscribe( documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) } - -// MARK: Async/Await - -/// This is the primary entry point function for fulfilling GraphQL operations -/// by parsing, validating, and executing a GraphQL 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 -/// 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 `Map` dictionary containing 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` if, for example, the query is invalid. 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. -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) -public func graphql( - queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), - mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), - subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, - schema: GraphQLSchema, - request: String, - rootValue: Any = (), - context: Any = (), - eventLoopGroup: EventLoopGroup, - variableValues: [String: Map] = [:], - operationName: String? = nil -) async throws -> GraphQLResult { - return try await graphql( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - request: request, - rootValue: rootValue, - context: context, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: operationName - ).get() -} - -/// 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. -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) -public func graphqlSubscribe( - queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), - mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), - subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, - schema: GraphQLSchema, - request: String, - rootValue: Any = (), - context: Any = (), - eventLoopGroup: EventLoopGroup, - variableValues: [String: Map] = [:], - operationName: String? = nil -) async throws -> SubscriptionResult { - return try await graphqlSubscribe( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - request: request, - rootValue: rootValue, - context: context, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: operationName - ).get() -} diff --git a/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift b/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift index 73d54278..f58ce3f5 100644 --- a/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift +++ b/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift @@ -1,5 +1,4 @@ import Dispatch -import NIO /// Proxies calls through to another `Instrumentation` instance via a DispatchQueue /// @@ -89,7 +88,6 @@ public class DispatchQueueInstrumentationWrapper: Instrumentation { schema: GraphQLSchema, document: Document, rootValue: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map], operation: OperationDefinition?, errors: [GraphQLError], @@ -104,7 +102,6 @@ public class DispatchQueueInstrumentationWrapper: Instrumentation { schema: schema, document: document, rootValue: rootValue, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operation: operation, errors: errors, @@ -120,9 +117,8 @@ public class DispatchQueueInstrumentationWrapper: Instrumentation { finished: DispatchTime, source: Any, args: Map, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo, - result: Result, Error> + result: Result ) { dispatchQueue.async(group: dispatchGroup) { self.instrumentation.fieldResolution( @@ -132,7 +128,6 @@ public class DispatchQueueInstrumentationWrapper: Instrumentation { finished: finished, source: source, args: args, - eventLoopGroup: eventLoopGroup, info: info, result: result ) diff --git a/Sources/GraphQL/Instrumentation/Instrumentation.swift b/Sources/GraphQL/Instrumentation/Instrumentation.swift index 1a315ab0..90fd04b0 100644 --- a/Sources/GraphQL/Instrumentation/Instrumentation.swift +++ b/Sources/GraphQL/Instrumentation/Instrumentation.swift @@ -1,6 +1,5 @@ import Dispatch import Foundation -import NIO /// Provides the capability to instrument the execution steps of a GraphQL query. /// @@ -35,7 +34,6 @@ public protocol Instrumentation { schema: GraphQLSchema, document: Document, rootValue: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map], operation: OperationDefinition?, errors: [GraphQLError], @@ -49,9 +47,8 @@ public protocol Instrumentation { finished: DispatchTime, source: Any, args: Map, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo, - result: Result, Error> + result: Result ) } @@ -105,7 +102,6 @@ struct noOpInstrumentation: Instrumentation { schema _: GraphQLSchema, document _: Document, rootValue _: Any, - eventLoopGroup _: EventLoopGroup, variableValues _: [String: Map], operation _: OperationDefinition?, errors _: [GraphQLError], @@ -119,8 +115,7 @@ struct noOpInstrumentation: Instrumentation { finished _: DispatchTime, source _: Any, args _: Map, - eventLoopGroup _: EventLoopGroup, info _: GraphQLResolveInfo, - result _: Result, Error> + result _: Result ) {} } diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift index 5ff7ad89..2f78093c 100644 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ b/Sources/GraphQL/Subscription/EventStream.swift @@ -3,7 +3,7 @@ open class EventStream { public init() {} /// Template method for mapping an event stream to a new generic type - MUST be overridden by /// implementing types. - open func map(_: @escaping (Element) throws -> To) -> EventStream { + open func map(_: @escaping (Element) async throws -> To) -> EventStream { fatalError("This function should be overridden by implementing classes") } } @@ -21,7 +21,7 @@ public class ConcurrentEventStream: EventStream { /// results. /// - Parameter closure: The closure to apply to each event in the stream /// - Returns: A stream of the results - override open func map(_ closure: @escaping (Element) throws -> To) + override open func map(_ closure: @escaping (Element) async throws -> To) -> ConcurrentEventStream { let newStream = stream.mapStream(closure) return ConcurrentEventStream(newStream) @@ -30,13 +30,13 @@ public class ConcurrentEventStream: EventStream { @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) extension AsyncThrowingStream { - func mapStream(_ closure: @escaping (Element) throws -> To) + func mapStream(_ closure: @escaping (Element) async throws -> To) -> AsyncThrowingStream { return AsyncThrowingStream { continuation in let task = Task { do { for try await event in self { - let newEvent = try closure(event) + let newEvent = try await closure(event) continuation.yield(newEvent) } continuation.finish() diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 8236b3d5..30dd61cb 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -1,4 +1,3 @@ -import NIO import OrderedCollections /** @@ -30,11 +29,10 @@ func subscribe( documentAST: Document, rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) -> EventLoopFuture { - let sourceFuture = createSourceEventStream( +) async throws -> SubscriptionResult { + let sourceResult = try await createSourceEventStream( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -43,38 +41,34 @@ func subscribe( documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) - return sourceFuture.map { sourceResult -> SubscriptionResult in - if let sourceStream = sourceResult.stream { - 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. - // 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. - execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - documentAST: documentAST, - rootValue: eventPayload, - context: context, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: operationName - ) - } - return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) - } else { - return SubscriptionResult(errors: sourceResult.errors) + if let sourceStream = sourceResult.stream { + let subscriptionStream = sourceStream.map { eventPayload -> GraphQLResult in + // 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. + try await execute( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + documentAST: documentAST, + rootValue: eventPayload, + context: context, + variableValues: variableValues, + operationName: operationName + ) } + return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) + } else { + return SubscriptionResult(errors: sourceResult.errors) } } @@ -114,10 +108,9 @@ func createSourceEventStream( documentAST: Document, rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) -> EventLoopFuture { +) async throws -> SourceEventStreamResult { let executeStarted = instrumentation.now do { @@ -132,11 +125,10 @@ func createSourceEventStream( documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, rawVariableValues: variableValues, operationName: operationName ) - return try executeSubscription(context: exeContext, eventLoopGroup: eventLoopGroup) + return try await executeSubscription(context: exeContext) } catch let error as GraphQLError { instrumentation.operationExecution( processId: processId(), @@ -146,24 +138,21 @@ func createSourceEventStream( schema: schema, document: documentAST, rootValue: rootValue, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operation: nil, errors: [error], result: nil ) - return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult(errors: [error])) + return SourceEventStreamResult(errors: [error]) } catch { - return eventLoopGroup.next() - .makeSucceededFuture(SourceEventStreamResult(errors: [GraphQLError(error)])) + return SourceEventStreamResult(errors: [GraphQLError(error)]) } } func executeSubscription( context: ExecutionContext, - eventLoopGroup: EventLoopGroup -) throws -> EventLoopFuture { +) async throws -> SourceEventStreamResult { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) var inputFields: OrderedDictionary = [:] @@ -233,17 +222,16 @@ func executeSubscription( // Get the resolve func, regardless of if its result is normal // or abrupt (error). - let resolvedFutureOrError = resolveOrError( + let resolvedOrError = await resolveOrError( resolve: resolve, source: context.rootValue, args: args, context: contextValue, - eventLoopGroup: eventLoopGroup, info: info ) - let resolvedFuture: Future - switch resolvedFutureOrError { + let resolved: Any? + switch resolvedOrError { case let .failure(error): if let graphQLError = error as? GraphQLError { throw graphQLError @@ -251,27 +239,25 @@ func executeSubscription( throw GraphQLError(error) } case let .success(success): - resolvedFuture = success + resolved = success } - return resolvedFuture.map { resolved -> SourceEventStreamResult in - if !context.errors.isEmpty { - return SourceEventStreamResult(errors: context.errors) - } else if let error = resolved as? GraphQLError { - return SourceEventStreamResult(errors: [error]) - } else if let stream = resolved as? EventStream { - return SourceEventStreamResult(stream: stream) - } else if resolved == nil { - return SourceEventStreamResult(errors: [ - GraphQLError(message: "Resolved subscription was nil"), - ]) - } else { - let resolvedObj = resolved as AnyObject - return SourceEventStreamResult(errors: [ - GraphQLError( - message: "Subscription field resolver must return EventStream. Received: '\(resolvedObj)'" - ), - ]) - } + if !context.errors.isEmpty { + return SourceEventStreamResult(errors: context.errors) + } else if let error = resolved as? GraphQLError { + return SourceEventStreamResult(errors: [error]) + } else if let stream = resolved as? EventStream { + return SourceEventStreamResult(stream: stream) + } else if resolved == nil { + return SourceEventStreamResult(errors: [ + GraphQLError(message: "Resolved subscription was nil"), + ]) + } else { + let resolvedObj = resolved as AnyObject + return SourceEventStreamResult(errors: [ + GraphQLError( + message: "Subscription field resolver must return EventStream. Received: '\(resolvedObj)'" + ), + ]) } } diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 7f2c0924..409d8344 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -1,5 +1,4 @@ import Foundation -import NIO import OrderedCollections /** @@ -414,13 +413,11 @@ public enum TypeResolveResult { public typealias GraphQLTypeResolve = ( _ value: Any, - _ eventLoopGroup: EventLoopGroup, _ info: GraphQLResolveInfo ) throws -> TypeResolveResultRepresentable public typealias GraphQLIsTypeOf = ( _ source: Any, - _ eventLoopGroup: EventLoopGroup, _ info: GraphQLResolveInfo ) throws -> Bool @@ -428,9 +425,8 @@ public typealias GraphQLFieldResolve = ( _ source: Any, _ args: Map, _ context: Any, - _ eventLoopGroup: EventLoopGroup, _ info: GraphQLResolveInfo -) throws -> Future +) async throws -> Any? public typealias GraphQLFieldResolveInput = ( _ source: Any, @@ -511,9 +507,9 @@ public struct GraphQLField { self.description = description self.astNode = astNode - self.resolve = { source, args, context, eventLoopGroup, info in + self.resolve = { source, args, context, info in let result = try resolve(source, args, context, info) - return eventLoopGroup.next().makeSucceededFuture(result) + return result } subscribe = nil } diff --git a/Sources/GraphQL/Type/Introspection.swift b/Sources/GraphQL/Type/Introspection.swift index 9841851d..89c2cc2f 100644 --- a/Sources/GraphQL/Type/Introspection.swift +++ b/Sources/GraphQL/Type/Introspection.swift @@ -1,4 +1,3 @@ -import NIO let __Schema = try! GraphQLObjectType( name: "__Schema", @@ -492,8 +491,8 @@ let SchemaMetaFieldDef = GraphQLFieldDefinition( name: "__schema", type: GraphQLNonNull(__Schema), description: "Access the current type schema of this server.", - resolve: { _, _, _, eventLoopGroup, info in - eventLoopGroup.next().makeSucceededFuture(info.schema) + resolve: { _, _, _, info in + info.schema } ) @@ -507,9 +506,9 @@ let TypeMetaFieldDef = GraphQLFieldDefinition( type: GraphQLNonNull(GraphQLString) ), ], - resolve: { _, arguments, _, eventLoopGroup, info in + resolve: { _, arguments, _, info in let name = arguments["name"].string! - return eventLoopGroup.next().makeSucceededFuture(info.schema.getType(name: name)) + return info.schema.getType(name: name) } ) @@ -517,8 +516,8 @@ let TypeNameMetaFieldDef = GraphQLFieldDefinition( name: "__typename", type: GraphQLNonNull(GraphQLString), description: "The name of the current Object type at runtime.", - resolve: { _, _, _, eventLoopGroup, info in - eventLoopGroup.next().makeSucceededFuture(info.parentType.name) + resolve: { _, _, _, info in + info.parentType.name } ) diff --git a/Sources/GraphQL/Utilities/NIO+Extensions.swift b/Sources/GraphQL/Utilities/NIO+Extensions.swift deleted file mode 100644 index 2131551a..00000000 --- a/Sources/GraphQL/Utilities/NIO+Extensions.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// NIO+Extensions.swift -// GraphQL -// -// Created by Jeff Seibert on 3/9/18. -// - -import Foundation -import NIO -import OrderedCollections - -public typealias Future = EventLoopFuture - -public extension Collection { - func flatten(on eventLoopGroup: EventLoopGroup) -> Future<[T]> where Element == Future { - return Future.whenAllSucceed(Array(self), on: eventLoopGroup.next()) - } -} - -extension Dictionary where Value: FutureType { - func flatten(on eventLoopGroup: EventLoopGroup) -> Future<[Key: Value.Expectation]> { - // create array of futures with (key,value) tuple - let futures: [Future<(Key, Value.Expectation)>] = map { element in - element.value.map(file: #file, line: #line) { (key: element.key, value: $0) } - } - // when all futures have succeeded convert tuple array back to dictionary - return EventLoopFuture.whenAllSucceed(futures, on: eventLoopGroup.next()).map { - .init(uniqueKeysWithValues: $0) - } - } -} - -extension OrderedDictionary where Value: FutureType { - func flatten(on eventLoopGroup: EventLoopGroup) - -> Future> - { - let keys = self.keys - // create array of futures with (key,value) tuple - let futures: [Future<(Key, Value.Expectation)>] = map { element in - element.value.map(file: #file, line: #line) { (key: element.key, value: $0) } - } - // when all futures have succeeded convert tuple array back to dictionary - return EventLoopFuture.whenAllSucceed(futures, on: eventLoopGroup.next()) - .map { unorderedResult in - var result: OrderedDictionary = [:] - for key in keys { - // Unwrap is guaranteed because keys are from original dictionary and maps - // preserve all elements - result[key] = unorderedResult.first(where: { $0.0 == key })!.1 - } - return result - } - } -} - -public protocol FutureType { - associatedtype Expectation - func whenSuccess(_ callback: @escaping @Sendable (Expectation) -> Void) - func whenFailure(_ callback: @escaping @Sendable (Error) -> Void) - func map( - file: StaticString, - line: UInt, - _ callback: @escaping (Expectation) -> (NewValue) - ) -> EventLoopFuture -} - -extension Future: FutureType { - public typealias Expectation = Value -} - -// Copied from https://github.com/vapor/async-kit/blob/e2f741640364c1d271405da637029ea6a33f754e/Sources/AsyncKit/EventLoopFuture/Future%2BTry.swift -// in order to avoid full package dependency. -public extension EventLoopFuture { - func tryFlatMap( - file _: StaticString = #file, line _: UInt = #line, - _ callback: @escaping (Value) throws -> EventLoopFuture - ) -> EventLoopFuture { - /// When the current `EventLoopFuture` is fulfilled, run the provided callback, - /// which will provide a new `EventLoopFuture`. - /// - /// This allows you to dynamically dispatch new asynchronous tasks as phases in a - /// longer series of processing steps. Note that you can use the results of the - /// current `EventLoopFuture` when determining how to dispatch the next operation. - /// - /// The key difference between this method and the regular `flatMap` is error handling. - /// - /// With `tryFlatMap`, the provided callback _may_ throw Errors, causing the returned - /// `EventLoopFuture` - /// to report failure immediately after the completion of the original `EventLoopFuture`. - flatMap { [eventLoop] value in - do { - return try callback(value) - } catch { - return eventLoop.makeFailedFuture(error) - } - } - } -} diff --git a/Tests/GraphQLTests/ExecutionTests/OneOfTests.swift b/Tests/GraphQLTests/ExecutionTests/OneOfTests.swift index 1b6a50d9..8c4bf25b 100644 --- a/Tests/GraphQLTests/ExecutionTests/OneOfTests.swift +++ b/Tests/GraphQLTests/ExecutionTests/OneOfTests.swift @@ -1,13 +1,10 @@ @testable import GraphQL -import NIO import XCTest class OneOfTests: XCTestCase { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - // MARK: OneOf Input Objects - func testAcceptsAGoodDefaultValue() throws { + func testAcceptsAGoodDefaultValue() async throws { let query = """ query ($input: TestInputObject! = {a: "abc"}) { test(input: $input) { @@ -16,11 +13,10 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual( result, GraphQLResult(data: [ @@ -32,7 +28,7 @@ class OneOfTests: XCTestCase { ) } - func testRejectsABadDefaultValue() throws { + func testRejectsABadDefaultValue() async throws { let query = """ query ($input: TestInputObject! = {a: "abc", b: 123}) { test(input: $input) { @@ -41,11 +37,10 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result.errors.count, 1) XCTAssertEqual( result.errors[0].message, @@ -53,7 +48,7 @@ class OneOfTests: XCTestCase { ) } - func testAcceptsAGoodVariable() throws { + func testAcceptsAGoodVariable() async throws { let query = """ query ($input: TestInputObject!) { test(input: $input) { @@ -62,12 +57,11 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), request: query, - eventLoopGroup: eventLoopGroup, variableValues: ["input": ["a": "abc"]] - ).wait() + ) XCTAssertEqual( result, GraphQLResult(data: [ @@ -79,7 +73,7 @@ class OneOfTests: XCTestCase { ) } - func testAcceptsAGoodVariableWithAnUndefinedKey() throws { + func testAcceptsAGoodVariableWithAnUndefinedKey() async throws { let query = """ query ($input: TestInputObject!) { test(input: $input) { @@ -88,12 +82,11 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), request: query, - eventLoopGroup: eventLoopGroup, variableValues: ["input": ["a": "abc", "b": .undefined]] - ).wait() + ) XCTAssertEqual( result, GraphQLResult(data: [ @@ -105,7 +98,7 @@ class OneOfTests: XCTestCase { ) } - func testRejectsAVariableWithMultipleNonNullKeys() throws { + func testRejectsAVariableWithMultipleNonNullKeys() async throws { let query = """ query ($input: TestInputObject!) { test(input: $input) { @@ -114,12 +107,11 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), request: query, - eventLoopGroup: eventLoopGroup, variableValues: ["input": ["a": "abc", "b": 123]] - ).wait() + ) XCTAssertEqual(result.errors.count, 1) XCTAssertEqual( result.errors[0].message, @@ -130,7 +122,7 @@ class OneOfTests: XCTestCase { ) } - func testRejectsAVariableWithMultipleNullableKeys() throws { + func testRejectsAVariableWithMultipleNullableKeys() async throws { let query = """ query ($input: TestInputObject!) { test(input: $input) { @@ -139,12 +131,11 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), request: query, - eventLoopGroup: eventLoopGroup, variableValues: ["input": ["a": "abc", "b": .null]] - ).wait() + ) XCTAssertEqual(result.errors.count, 1) XCTAssertEqual( result.errors[0].message, @@ -163,7 +154,7 @@ func getSchema() throws -> GraphQLSchema { "a": GraphQLField(type: GraphQLString), "b": GraphQLField(type: GraphQLInt), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is TestObject } ) diff --git a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift index cd021f3b..cf8e09e1 100644 --- a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift +++ b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift @@ -1,6 +1,5 @@ import Dispatch @testable import GraphQL -import NIO import XCTest class FieldExecutionStrategyTests: XCTestCase { @@ -14,16 +13,14 @@ class FieldExecutionStrategyTests: XCTestCase { fields: [ "sleep": GraphQLField( type: GraphQLString, - resolve: { _, _, _, eventLoopGroup, _ in - eventLoopGroup.next().makeSucceededVoidFuture().map { - Thread.sleep(forTimeInterval: 0.1) - return "z" - } + resolve: { _, _, _, _ in + Thread.sleep(forTimeInterval: 0.1) + return "z" } ), "bang": GraphQLField( type: GraphQLString, - resolve: { (_, _, _, _, info: GraphQLResolveInfo) in + resolve: { (_, _, _, info: GraphQLResolveInfo) in let group = DispatchGroup() group.enter() @@ -40,7 +37,7 @@ class FieldExecutionStrategyTests: XCTestCase { ), "futureBang": GraphQLField( type: GraphQLString, - resolve: { (_, _, _, eventLoopGroup, info: GraphQLResolveInfo) in + resolve: { (_, _, _, info: GraphQLResolveInfo) in let g = DispatchGroup() g.enter() @@ -50,9 +47,9 @@ class FieldExecutionStrategyTests: XCTestCase { g.wait() - return eventLoopGroup.next().makeFailedFuture(StrategyError.exampleError( + throw StrategyError.exampleError( msg: "\(info.fieldName): \(info.path.elements.last!)" - )) + ) } ), ] @@ -184,9 +181,10 @@ class FieldExecutionStrategyTests: XCTestCase { ), ] - func timing(_ block: @autoclosure () throws -> T) throws -> (value: T, seconds: Double) { + func timing(_ block: @autoclosure () async throws -> T) async throws + -> (value: T, seconds: Double) { let start = DispatchTime.now() - let value = try block() + let value = try await block() let nanoseconds = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds let seconds = Double(nanoseconds) / 1_000_000_000 return ( @@ -195,67 +193,52 @@ class FieldExecutionStrategyTests: XCTestCase { ) } - private var eventLoopGroup: EventLoopGroup! - - override func setUp() { - eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } - - override func tearDown() { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - - func testSerialFieldExecutionStrategyWithSingleField() throws { - let result = try timing(graphql( + func testSerialFieldExecutionStrategyWithSingleField() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: singleQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleQuery + )) XCTAssertEqual(result.value, singleExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testSerialFieldExecutionStrategyWithSingleFieldError() throws { - let result = try timing(graphql( + func testSerialFieldExecutionStrategyWithSingleFieldError() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: singleThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleThrowsQuery + )) XCTAssertEqual(result.value, singleThrowsExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testSerialFieldExecutionStrategyWithSingleFieldFailedFuture() throws { - let result = try timing(graphql( + func testSerialFieldExecutionStrategyWithSingleFieldFailedFuture() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: singleFailedFutureQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleFailedFutureQuery + )) XCTAssertEqual(result.value, singleFailedFutureExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testSerialFieldExecutionStrategyWithMultipleFields() throws { - let result = try timing(graphql( + func testSerialFieldExecutionStrategyWithMultipleFields() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: multiQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: multiQuery + )) XCTAssertEqual(result.value, multiExpected) // XCTAssertEqualWithAccuracy(1.0, result.seconds, accuracy: 0.5) } - func testSerialFieldExecutionStrategyWithMultipleFieldErrors() throws { - let result = try timing(graphql( + func testSerialFieldExecutionStrategyWithMultipleFieldErrors() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: multiThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: multiThrowsQuery + )) XCTAssertEqual(result.value.data, multiThrowsExpectedData) let resultErrors = result.value.errors XCTAssertEqual(resultErrors.count, multiThrowsExpectedErrors.count) @@ -265,46 +248,42 @@ class FieldExecutionStrategyTests: XCTestCase { // XCTAssertEqualWithAccuracy(1.0, result.seconds, accuracy: 0.5) } - func testConcurrentDispatchFieldExecutionStrategyWithSingleField() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), + func testConcurrentFieldExecutionStrategyWithSingleField() async throws { + let result = try await timing(await graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), schema: schema, - request: singleQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleQuery + )) XCTAssertEqual(result.value, singleExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testConcurrentDispatchFieldExecutionStrategyWithSingleFieldError() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), + func testConcurrentFieldExecutionStrategyWithSingleFieldError() async throws { + let result = try await timing(await graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), schema: schema, - request: singleThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleThrowsQuery + )) XCTAssertEqual(result.value, singleThrowsExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testConcurrentDispatchFieldExecutionStrategyWithMultipleFields() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), + func testConcurrentFieldExecutionStrategyWithMultipleFields() async throws { + let result = try await timing(await graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), schema: schema, - request: multiQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: multiQuery + )) XCTAssertEqual(result.value, multiExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testConcurrentDispatchFieldExecutionStrategyWithMultipleFieldErrors() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), + func testConcurrentFieldExecutionStrategyWithMultipleFieldErrors() async throws { + let result = try await timing(await graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), schema: schema, - request: multiThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: multiThrowsQuery + )) XCTAssertEqual(result.value.data, multiThrowsExpectedData) let resultErrors = result.value.errors XCTAssertEqual(resultErrors.count, multiThrowsExpectedErrors.count) diff --git a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift index de6252ea..c4da2977 100644 --- a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift @@ -1,5 +1,4 @@ @testable import GraphQL -import NIO import XCTest class HelloWorldTests: XCTestCase { @@ -17,32 +16,19 @@ class HelloWorldTests: XCTestCase { ) ) - func testHello() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - + func testHello() async throws { let query = "{ hello }" let expected = GraphQLResult(data: ["hello": "world"]) - let result = try graphql( + let result = try await graphql( schema: schema, - request: query, - eventLoopGroup: group - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testBoyhowdy() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - + func testBoyhowdy() async throws { let query = "{ boyhowdy }" let expected = GraphQLResult( @@ -54,30 +40,22 @@ class HelloWorldTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: schema, - request: query, - eventLoopGroup: group - ).wait() + request: query + ) XCTAssertEqual(result, expected) } @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) func testHelloAsync() async throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - let query = "{ hello }" let expected = GraphQLResult(data: ["hello": "world"]) let result = try await graphql( schema: schema, - request: query, - eventLoopGroup: group + request: query ) XCTAssertEqual(result, expected) diff --git a/Tests/GraphQLTests/InputTests/InputTests.swift b/Tests/GraphQLTests/InputTests/InputTests.swift index fe66d461..0c9fd2b7 100644 --- a/Tests/GraphQLTests/InputTests/InputTests.swift +++ b/Tests/GraphQLTests/InputTests/InputTests.swift @@ -1,9 +1,8 @@ @testable import GraphQL -import NIO import XCTest class InputTests: XCTestCase { - func testArgsNonNullNoDefault() throws { + func testArgsNonNullNoDefault() async throws { struct Echo: Codable { let field1: String } @@ -20,7 +19,7 @@ class InputTests: XCTestCase { type: GraphQLNonNull(GraphQLString) ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -48,49 +47,44 @@ class InputTests: XCTestCase { types: [EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test basic functionality - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: "value1" - ) { - field1 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": "value1", - ] - ).wait(), + } + """, + variableValues: [ + "field1": "value1", + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -99,77 +93,73 @@ class InputTests: XCTestCase { ) // Test providing null results in an error - XCTAssertTrue( - try graphql( - schema: schema, - request: """ - { - echo( - field1: null - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 } - """, - eventLoopGroup: group - ).wait() - .errors.count > 0 + } + """ ) XCTAssertTrue( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result.errors.count > 0 + ) + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": .null, - ] - ).wait() - .errors.count > 0 + } + """, + variableValues: [ + "field1": .null, + ] + ) + XCTAssertTrue( + result.errors.count > 0 ) // Test not providing parameter results in an error - XCTAssertTrue( - try graphql( - schema: schema, - request: """ - { - echo { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo { + field1 } - """, - eventLoopGroup: group - ).wait() - .errors.count > 0 + } + """ ) XCTAssertTrue( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result.errors.count > 0 + ) + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait() - .errors.count > 0 + } + """, + variableValues: [:] + ) + XCTAssertTrue( + result.errors.count > 0 ) } - func testArgsNullNoDefault() throws { + func testArgsNullNoDefault() async throws { struct Echo: Codable { let field1: String? } @@ -186,7 +176,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -214,49 +204,44 @@ class InputTests: XCTestCase { types: [EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test basic functionality - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: "value1" - ) { - field1 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": "value1", - ] - ).wait(), + } + """, + variableValues: [ + "field1": "value1", + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -265,43 +250,43 @@ class InputTests: XCTestCase { ) // Test providing null is accepted - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: null - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": .null, - ] - ).wait(), + } + """, + variableValues: [ + "field1": .null, + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, @@ -310,39 +295,39 @@ class InputTests: XCTestCase { ) // Test not providing parameter is accepted - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait(), + } + """, + variableValues: [:] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, @@ -351,7 +336,7 @@ class InputTests: XCTestCase { ) } - func testArgsNonNullDefault() throws { + func testArgsNonNullDefault() async throws { struct Echo: Codable { let field1: String } @@ -368,7 +353,7 @@ class InputTests: XCTestCase { type: GraphQLNonNull(GraphQLString) ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -397,49 +382,44 @@ class InputTests: XCTestCase { types: [EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test basic functionality - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: "value1" - ) { - field1 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": "value1", - ] - ).wait(), + } + """, + variableValues: [ + "field1": "value1", + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -448,76 +428,74 @@ class InputTests: XCTestCase { ) // Test providing null results in an error - XCTAssertTrue( - try graphql( - schema: schema, - request: """ - { - echo( - field1: null - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 } - """, - eventLoopGroup: group - ).wait() - .errors.count > 0 + } + """ ) XCTAssertTrue( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result.errors.count > 0 + ) + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": .null, - ] - ).wait() - .errors.count > 0 + } + """, + variableValues: [ + "field1": .null, + ] + ) + XCTAssertTrue( + result.errors.count > 0 ) // Test not providing parameter results in default - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "defaultValue1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String! = "defaultValue1") { - echo ( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String! = "defaultValue1") { + echo ( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait(), + } + """, + variableValues: [:] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "defaultValue1", @@ -526,26 +504,25 @@ class InputTests: XCTestCase { ) // Test variable doesn't get argument default - XCTAssertTrue( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo ( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo ( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait() - .errors.count > 0 + } + """, + variableValues: [:] + ) + XCTAssertTrue( + result.errors.count > 0 ) } - func testArgsNullDefault() throws { + func testArgsNullDefault() async throws { struct Echo: Codable { let field1: String? } @@ -562,7 +539,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -591,49 +568,44 @@ class InputTests: XCTestCase { types: [EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test basic functionality - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: "value1" - ) { - field1 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": "value1", - ] - ).wait(), + } + """, + variableValues: [ + "field1": "value1", + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -642,43 +614,43 @@ class InputTests: XCTestCase { ) // Test providing null results in a null output - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: null - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": .null, - ] - ).wait(), + } + """, + variableValues: [ + "field1": .null, + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, @@ -687,39 +659,39 @@ class InputTests: XCTestCase { ) // Test not providing parameter results in default - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "defaultValue1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String = "defaultValue1") { - echo ( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String = "defaultValue1") { + echo ( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait(), + } + """, + variableValues: [:] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "defaultValue1", @@ -728,21 +700,21 @@ class InputTests: XCTestCase { ) // Test that nullable unprovided variables are coerced to null - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo ( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo ( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait(), + } + """, + variableValues: [:] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, @@ -752,7 +724,7 @@ class InputTests: XCTestCase { } // Test that input objects parse as expected from non-null literals - func testInputNoNull() throws { + func testInputNoNull() async throws { struct Echo: Codable { let field1: String? let field2: String? @@ -798,7 +770,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -827,28 +799,23 @@ class InputTests: XCTestCase { types: [EchoInputType, EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test in arguments - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1", - field2: "value2", - }) { - field1 - field2 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1", + field2: "value2", + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -858,25 +825,25 @@ class InputTests: XCTestCase { ) // Test in variables - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - "field2": "value2", - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + "field2": "value2", + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -887,7 +854,7 @@ class InputTests: XCTestCase { } // Test that inputs parse as expected when null literals are present - func testInputParsingDefinedNull() throws { + func testInputParsingDefinedNull() async throws { struct Echo: Codable { let field1: String? let field2: String? @@ -933,7 +900,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -962,28 +929,23 @@ class InputTests: XCTestCase { types: [EchoInputType, EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test in arguments - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1", - field2: null, - }) { - field1 - field2 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1", + field2: null, + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -993,25 +955,25 @@ class InputTests: XCTestCase { ) // Test in variables - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - "field2": .null, - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + "field2": .null, + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1022,7 +984,7 @@ class InputTests: XCTestCase { } // Test that input objects parse as expected when there are missing fields with no default - func testInputParsingUndefined() throws { + func testInputParsingUndefined() async throws { struct Echo: Codable { let field1: String? let field2: String? @@ -1073,7 +1035,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -1102,27 +1064,22 @@ class InputTests: XCTestCase { types: [EchoInputType, EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test in arguments - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1" - }) { - field1 - field2 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1" + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1132,24 +1089,24 @@ class InputTests: XCTestCase { ) // Test in variables - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1160,7 +1117,7 @@ class InputTests: XCTestCase { } // Test that input objects parse as expected when there are missing fields with defaults - func testInputParsingUndefinedWithDefault() throws { + func testInputParsingUndefinedWithDefault() async throws { struct Echo: Codable { let field1: String? let field2: String? @@ -1207,7 +1164,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -1236,27 +1193,22 @@ class InputTests: XCTestCase { types: [EchoInputType, EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Undefined with default gets default - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1" - }) { - field1 - field2 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1" + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1265,22 +1217,22 @@ class InputTests: XCTestCase { ]) ) // Null literal with default gets null - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1" - field2: null - }) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1" + field2: null + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1291,24 +1243,24 @@ class InputTests: XCTestCase { // Test in variable // Undefined with default gets default - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1317,25 +1269,25 @@ class InputTests: XCTestCase { ]) ) // Null literal with default gets null - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - "field2": .null, - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + "field2": .null, + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", diff --git a/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift b/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift index 56a77b93..3583aa7f 100644 --- a/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift +++ b/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift @@ -1,7 +1,6 @@ import Dispatch import Foundation import GraphQL -import NIO import XCTest class InstrumentationTests: XCTestCase, Instrumentation { @@ -101,7 +100,6 @@ class InstrumentationTests: XCTestCase, Instrumentation { schema _: GraphQLSchema, document _: Document, rootValue _: Any, - eventLoopGroup _: EventLoopGroup, variableValues _: [String: Map], operation _: OperationDefinition?, errors _: [GraphQLError], @@ -128,9 +126,8 @@ class InstrumentationTests: XCTestCase, Instrumentation { finished _: DispatchTime, source _: Any, args _: Map, - eventLoopGroup _: EventLoopGroup, info _: GraphQLResolveInfo, - result _: Result, Error> + result _: Result ) { fieldResolutionCalled += 1 // XCTAssertEqual(processId, expectedProcessId, "unexpected process id") diff --git a/Tests/GraphQLTests/StarWarsTests/StarWarsIntrospectionTests.swift b/Tests/GraphQLTests/StarWarsTests/StarWarsIntrospectionTests.swift index ff8b4b5b..3ce7a76e 100644 --- a/Tests/GraphQLTests/StarWarsTests/StarWarsIntrospectionTests.swift +++ b/Tests/GraphQLTests/StarWarsTests/StarWarsIntrospectionTests.swift @@ -1,15 +1,9 @@ -import NIO import XCTest @testable import GraphQL class StarWarsIntrospectionTests: XCTestCase { - func testIntrospectionTypeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionTypeQuery() async throws { do { let query = "query IntrospectionTypeQuery {" + " __schema {" + @@ -73,23 +67,17 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } catch { print(error) } } - func testIntrospectionQueryTypeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionQueryTypeQuery() async throws { let query = "query IntrospectionQueryTypeQuery {" + " __schema {" + " queryType {" + @@ -108,20 +96,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidTypeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidTypeQuery() async throws { let query = "query IntrospectionDroidTypeQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -136,20 +118,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidKindQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidKindQuery() async throws { let query = "query IntrospectionDroidKindQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -166,20 +142,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionCharacterKindQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionCharacterKindQuery() async throws { let query = "query IntrospectionCharacterKindQuery {" + " __type(name: \"Character\") {" + " name" + @@ -196,20 +166,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidFieldsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidFieldsQuery() async throws { let query = "query IntrospectionDroidFieldsQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -275,20 +239,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidNestedFieldsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidNestedFieldsQuery() async throws { let query = "query IntrospectionDroidNestedFieldsQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -373,20 +331,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionFieldArgsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionFieldArgsQuery() async throws { let query = "query IntrospectionFieldArgsQuery {" + " __schema {" + " queryType {" + @@ -472,20 +424,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidDescriptionQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidDescriptionQuery() async throws { let query = "query IntrospectionDroidDescriptionQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -502,11 +448,10 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } } diff --git a/Tests/GraphQLTests/StarWarsTests/StarWarsQueryTests.swift b/Tests/GraphQLTests/StarWarsTests/StarWarsQueryTests.swift index 368436ad..d0053524 100644 --- a/Tests/GraphQLTests/StarWarsTests/StarWarsQueryTests.swift +++ b/Tests/GraphQLTests/StarWarsTests/StarWarsQueryTests.swift @@ -1,16 +1,9 @@ -import NIO import XCTest @testable import GraphQL class StarWarsQueryTests: XCTestCase { - func testHeroNameQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testHeroNameQuery() async throws { let query = """ query HeroNameQuery { hero { @@ -27,22 +20,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testHeroNameAndFriendsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testHeroNameAndFriendsQuery() async throws { let query = """ query HeroNameAndFriendsQuery { hero { @@ -69,22 +55,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testNestedQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testNestedQuery() async throws { let query = """ query NestedQuery { hero { @@ -139,20 +118,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testFetchLukeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testFetchLukeQuery() async throws { let query = """ query FetchLukeQuery { @@ -170,20 +143,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testOptionalVariable() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testOptionalVariable() async throws { let query = """ query FetchHeroByEpisodeQuery($episode: Episode) { @@ -208,12 +175,11 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) // or we can pass "EMPIRE" and expect Luke @@ -229,21 +195,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) } - func testFetchSomeIDQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testFetchSomeIDQuery() async throws { let query = """ query FetchSomeIDQuery($someId: String!) { @@ -269,12 +229,11 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) params = [ @@ -289,12 +248,11 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) params = [ @@ -307,21 +265,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) } - func testFetchLukeAliasedQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testFetchLukeAliasedQuery() async throws { let query = """ query FetchLukeAliasedQuery { @@ -339,20 +291,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testFetchLukeAndLeiaAliasedQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testFetchLukeAndLeiaAliasedQuery() async throws { let query = """ query FetchLukeAndLeiaAliasedQuery { @@ -376,20 +322,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testDuplicateFieldsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testDuplicateFieldsQuery() async throws { let query = """ query DuplicateFieldsQuery { @@ -417,20 +357,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testUseFragmentQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testUseFragmentQuery() async throws { let query = """ query UseFragmentQuery { @@ -460,20 +394,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testCheckTypeOfR2Query() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testCheckTypeOfR2Query() async throws { let query = """ query CheckTypeOfR2Query { @@ -493,21 +421,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testCheckTypeOfLukeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testCheckTypeOfLukeQuery() async throws { let query = """ query CheckTypeOfLukeQuery { @@ -527,20 +449,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testSecretBackstoryQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testSecretBackstoryQuery() async throws { let query = """ query SecretBackstoryQuery { @@ -567,20 +483,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testSecretBackstoryListQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testSecretBackstoryListQuery() async throws { let query = """ query SecretBackstoryListQuery { @@ -633,20 +543,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testSecretBackstoryAliasQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testSecretBackstoryAliasQuery() async throws { let query = """ query SecretBackstoryAliasQuery { @@ -673,20 +577,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testNonNullableFieldsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testNonNullableFieldsQuery() async throws { let A = try GraphQLObjectType( name: "A", fields: [:] @@ -762,19 +660,13 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql(schema: schema, request: query, eventLoopGroup: eventLoopGroup) - .wait() + let result = try await graphql(schema: schema, request: query) + XCTAssertEqual(result, expected) } - func testFieldOrderQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - - XCTAssertEqual(try graphql( + func testFieldOrderQuery() async throws { + var result = try await graphql( schema: starWarsSchema, request: """ query HeroNameQuery { @@ -783,9 +675,9 @@ class StarWarsQueryTests: XCTestCase { name } } - """, - eventLoopGroup: eventLoopGroup - ).wait(), GraphQLResult( + """ + ) + XCTAssertEqual(result, GraphQLResult( data: [ "hero": [ "id": "2001", @@ -794,7 +686,7 @@ class StarWarsQueryTests: XCTestCase { ] )) - XCTAssertNotEqual(try graphql( + result = try await graphql( schema: starWarsSchema, request: """ query HeroNameQuery { @@ -803,9 +695,9 @@ class StarWarsQueryTests: XCTestCase { name } } - """, - eventLoopGroup: eventLoopGroup - ).wait(), GraphQLResult( + """ + ) + XCTAssertNotEqual(result, GraphQLResult( data: [ "hero": [ "name": "R2-D2", diff --git a/Tests/GraphQLTests/StarWarsTests/StarWarsSchema.swift b/Tests/GraphQLTests/StarWarsTests/StarWarsSchema.swift index d9d70562..23841438 100644 --- a/Tests/GraphQLTests/StarWarsTests/StarWarsSchema.swift +++ b/Tests/GraphQLTests/StarWarsTests/StarWarsSchema.swift @@ -111,7 +111,7 @@ let CharacterInterface = try! GraphQLInterfaceType( description: "All secrets about their past." ), ] }, - resolveType: { character, _, _ in + resolveType: { character, _ in switch character { case is Human: return "Human" @@ -174,7 +174,7 @@ let HumanType = try! GraphQLObjectType( ), ], interfaces: [CharacterInterface], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Human } ) @@ -232,7 +232,7 @@ let DroidType = try! GraphQLObjectType( ), ], interfaces: [CharacterInterface], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Droid } ) diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index fb940c72..b535fc43 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -1,5 +1,4 @@ @testable import GraphQL -import NIO // MARK: Types @@ -89,8 +88,6 @@ let EmailQueryType = try! GraphQLObjectType( // MARK: Test Helpers -let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) class EmailDb { var emails: [Email] @@ -121,17 +118,17 @@ class EmailDb { /// Returns the default email schema, with standard resolvers. func defaultSchema() throws -> GraphQLSchema { return try emailSchemaWithResolvers( - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + resolve: { emailAny, _, _, _ throws -> Any? in if let email = emailAny as? Email { - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return EmailEvent( email: email, inbox: Inbox(emails: self.emails) - )) + ) } else { throw GraphQLError(message: "\(type(of: emailAny)) is not Email") } }, - subscribe: { _, args, _, eventLoopGroup, _ throws -> EventLoopFuture in + subscribe: { _, args, _, _ throws -> Any? in let priority = args["priority"].int ?? 0 let filtered = self.publisher.subscribe().stream .filterStream { emailAny throws in @@ -141,8 +138,7 @@ class EmailDb { return true } } - return eventLoopGroup.next() - .makeSucceededFuture(ConcurrentEventStream(filtered)) + return ConcurrentEventStream(filtered) } ) } @@ -151,8 +147,8 @@ class EmailDb { func subscription( query: String, variableValues: [String: Map] = [:] - ) throws -> SubscriptionEventStream { - return try createSubscription( + ) async throws -> SubscriptionEventStream { + return try await createSubscription( schema: defaultSchema(), query: query, variableValues: variableValues @@ -191,8 +187,8 @@ func createSubscription( schema: GraphQLSchema, query: String, variableValues: [String: Map] = [:] -) throws -> SubscriptionEventStream { - let result = try graphqlSubscribe( +) async throws -> SubscriptionEventStream { + let result = try await graphqlSubscribe( queryStrategy: SerialFieldExecutionStrategy(), mutationStrategy: SerialFieldExecutionStrategy(), subscriptionStrategy: SerialFieldExecutionStrategy(), @@ -201,10 +197,9 @@ func createSubscription( request: query, rootValue: (), context: (), - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: nil - ).wait() + ) if let stream = result.stream { return stream diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index db0ebfb6..fc52ffea 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -1,5 +1,4 @@ import GraphQL -import NIO import XCTest /// This follows the graphql-js testing, with deviations where noted. @@ -30,9 +29,8 @@ class SubscriptionTests: XCTestCase { let subscriptionResult = try await graphqlSubscribe( schema: schema, - request: query, - eventLoopGroup: eventLoopGroup - ).get() + request: query + ) guard let subscription = subscriptionResult.stream else { XCTFail(subscriptionResult.errors.description) return @@ -50,7 +48,7 @@ class SubscriptionTests: XCTestCase { unread: true )) db.stop() - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( @@ -85,23 +83,19 @@ class SubscriptionTests: XCTestCase { type: GraphQLInt ), ], - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + resolve: { emailAny, _, _, _ throws -> Any? in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" ) } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return EmailEvent( email: email, inbox: Inbox(emails: db.emails) - )) + ) }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ throws -> Any? in + db.publisher.subscribe() } ), "notImportantEmail": GraphQLField( @@ -111,29 +105,25 @@ class SubscriptionTests: XCTestCase { type: GraphQLInt ), ], - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + resolve: { emailAny, _, _, _ throws -> Any? in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" ) } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return EmailEvent( email: email, inbox: Inbox(emails: db.emails) - )) + ) }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ throws -> Any? in + db.publisher.subscribe() } ), ] ) ) - let subscription = try createSubscription(schema: schema, query: """ + let subscription = try await createSubscription(schema: schema, query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -160,7 +150,7 @@ class SubscriptionTests: XCTestCase { unread: true )) - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( @@ -195,34 +185,28 @@ class SubscriptionTests: XCTestCase { fields: [ "importantEmail": GraphQLField( type: EmailEventType, - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) + resolve: { _, _, _, _ throws -> Any? in + nil }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + subscribe: { _, _, _, _ throws -> Any? in didResolveImportantEmail = true - return eventLoopGroup.next() - .makeSucceededFuture(db.publisher.subscribe()) + return db.publisher.subscribe() } ), "notImportantEmail": GraphQLField( type: EmailEventType, - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) + resolve: { _, _, _, _ throws -> Any? in + nil }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + subscribe: { _, _, _, _ throws -> Any? in didResolveNonImportantEmail = true - return eventLoopGroup.next() - .makeSucceededFuture(db.publisher.subscribe()) + return db.publisher.subscribe() } ), ] ) ) - let _ = try createSubscription(schema: schema, query: """ + let _ = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -256,15 +240,16 @@ class SubscriptionTests: XCTestCase { // Not implemented because this is taken care of by Swift optional types /// 'resolves to an error for unknown subscription field' - func testErrorUnknownSubscriptionField() throws { + func testErrorUnknownSubscriptionField() async throws { let db = EmailDb() - XCTAssertThrowsError( - try db.subscription(query: """ + do { + _ = try await db.subscription(query: """ subscription { unknownField } """) - ) { error in + XCTFail("Error should have been thrown") + } catch { guard let graphQLError = error as? GraphQLError else { XCTFail("Error was not of type GraphQLError") return @@ -278,25 +263,26 @@ class SubscriptionTests: XCTestCase { } /// 'should pass through unexpected errors thrown in subscribe' - func testPassUnexpectedSubscribeErrors() throws { + func testPassUnexpectedSubscribeErrors() async throws { let db = EmailDb() - XCTAssertThrowsError( - try db.subscription(query: "") - ) + do { + _ = try await db.subscription(query: "") + XCTFail("Error should have been thrown") + } catch {} } /// 'throws an error if subscribe does not return an iterator' - func testErrorIfSubscribeIsntIterator() throws { + func testErrorIfSubscribeIsntIterator() async throws { let schema = try emailSchemaWithResolvers( - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) + resolve: { _, _, _, _ throws -> Any? in + nil }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture("test") + subscribe: { _, _, _, _ throws -> Any? in + "test" } ) - XCTAssertThrowsError( - try createSubscription(schema: schema, query: """ + do { + _ = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -305,7 +291,8 @@ class SubscriptionTests: XCTestCase { } } """) - ) { error in + XCTFail("Error should have been thrown") + } catch { guard let graphQLError = error as? GraphQLError else { XCTFail("Error was not of type GraphQLError") return @@ -318,10 +305,10 @@ class SubscriptionTests: XCTestCase { } /// 'resolves to an error for subscription resolver errors' - func testErrorForSubscriptionResolverErrors() throws { - func verifyError(schema: GraphQLSchema) { - XCTAssertThrowsError( - try createSubscription(schema: schema, query: """ + func testErrorForSubscriptionResolverErrors() async throws { + func verifyError(schema: GraphQLSchema) async throws { + do { + _ = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -330,7 +317,8 @@ class SubscriptionTests: XCTestCase { } } """) - ) { error in + XCTFail("Error should have been thrown") + } catch { guard let graphQLError = error as? GraphQLError else { XCTFail("Error was not of type GraphQLError") return @@ -340,23 +328,23 @@ class SubscriptionTests: XCTestCase { } // Throwing an error - try verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, _, _ throws -> EventLoopFuture in + try await verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, _ throws -> Any? in throw GraphQLError(message: "test error") } )) // Resolving to an error - try verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(GraphQLError(message: "test error")) + try await verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, _ throws -> Any? in + GraphQLError(message: "test error") } )) // Rejecting with an error - try verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeFailedFuture(GraphQLError(message: "test error")) + try await verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, _ throws -> Any? in + GraphQLError(message: "test error") } )) } @@ -365,7 +353,7 @@ class SubscriptionTests: XCTestCase { // Tests above cover this /// 'resolves to an error if variables were wrong type' - func testErrorVariablesWrongType() throws { + func testErrorVariablesWrongType() async throws { let db = EmailDb() let query = """ subscription ($priority: Int) { @@ -382,14 +370,15 @@ class SubscriptionTests: XCTestCase { } """ - XCTAssertThrowsError( - try db.subscription( + do { + _ = try await db.subscription( query: query, variableValues: [ "priority": "meow", ] ) - ) { error in + XCTFail("Should have thrown error") + } catch { guard let graphQLError = error as? GraphQLError else { XCTFail("Error was not of type GraphQLError") return @@ -406,7 +395,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload for a single subscriber' func testSingleSubscriber() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -434,7 +423,7 @@ class SubscriptionTests: XCTestCase { )) db.stop() - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( @@ -455,7 +444,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload for multiple subscribe in same subscription' func testMultipleSubscribers() async throws { let db = EmailDb() - let subscription1 = try db.subscription(query: """ + let subscription1 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -474,7 +463,7 @@ class SubscriptionTests: XCTestCase { return } - let subscription2 = try db.subscription(query: """ + let subscription2 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -503,8 +492,8 @@ class SubscriptionTests: XCTestCase { unread: true )) - let result1 = try await iterator1.next()?.get() - let result2 = try await iterator2.next()?.get() + let result1 = try await iterator1.next() + let result2 = try await iterator2.next() let expected = GraphQLResult( data: ["importantEmail": [ @@ -526,7 +515,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload per subscription event' func testPayloadPerEvent() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -553,7 +542,7 @@ class SubscriptionTests: XCTestCase { message: "Tests are good", unread: true )) - let result1 = try await iterator.next()?.get() + let result1 = try await iterator.next() XCTAssertEqual( result1, GraphQLResult( @@ -577,7 +566,7 @@ class SubscriptionTests: XCTestCase { message: "I <3 making things", unread: true )) - let result2 = try await iterator.next()?.get() + let result2 = try await iterator.next() XCTAssertEqual( result2, GraphQLResult( @@ -599,7 +588,7 @@ class SubscriptionTests: XCTestCase { /// This is not in the graphql-js tests. func testArguments() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 5) { importantEmail(priority: $priority) { email { @@ -623,11 +612,9 @@ class SubscriptionTests: XCTestCase { // So that the Task won't immediately be cancelled since the ConcurrentEventStream is // discarded - let keepForNow = stream.map { event in - event.map { result in - results.append(result) - expectation.fulfill() - } + let keepForNow = stream.map { result in + results.append(result) + expectation.fulfill() } var expected = [GraphQLResult]() @@ -703,7 +690,7 @@ class SubscriptionTests: XCTestCase { /// 'should not trigger when subscription is already done' func testNoTriggerAfterDone() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -726,11 +713,9 @@ class SubscriptionTests: XCTestCase { var expectation = XCTestExpectation() // So that the Task won't immediately be cancelled since the ConcurrentEventStream is // discarded - let keepForNow = stream.map { event in - event.map { result in - results.append(result) - expectation.fulfill() - } + let keepForNow = stream.map { result in + results.append(result) + expectation.fulfill() } var expected = [GraphQLResult]() @@ -784,7 +769,7 @@ class SubscriptionTests: XCTestCase { /// 'event order is correct for multiple publishes' func testOrderCorrectForMultiplePublishes() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -817,7 +802,7 @@ class SubscriptionTests: XCTestCase { unread: true )) - let result1 = try await iterator.next()?.get() + let result1 = try await iterator.next() XCTAssertEqual( result1, GraphQLResult( @@ -834,7 +819,7 @@ class SubscriptionTests: XCTestCase { ) ) - let result2 = try await iterator.next()?.get() + let result2 = try await iterator.next() XCTAssertEqual( result2, GraphQLResult( @@ -857,7 +842,7 @@ class SubscriptionTests: XCTestCase { let db = EmailDb() let schema = try emailSchemaWithResolvers( - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + resolve: { emailAny, _, _, _ throws -> Any? in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" @@ -866,17 +851,17 @@ class SubscriptionTests: XCTestCase { if email.subject == "Goodbye" { // Force the system to fail here. throw GraphQLError(message: "Never leave.") } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return EmailEvent( email: email, inbox: Inbox(emails: db.emails) - )) + ) }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ throws -> Any? in + db.publisher.subscribe() } ) - let subscription = try createSubscription(schema: schema, query: """ + let subscription = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -894,11 +879,9 @@ class SubscriptionTests: XCTestCase { var expectation = XCTestExpectation() // So that the Task won't immediately be cancelled since the ConcurrentEventStream is // discarded - let keepForNow = stream.map { event in - event.map { result in - results.append(result) - expectation.fulfill() - } + let keepForNow = stream.map { result in + results.append(result) + expectation.fulfill() } var expected = [GraphQLResult]() @@ -971,7 +954,7 @@ class SubscriptionTests: XCTestCase { /// Test incorrect emitted type errors func testErrorWrongEmitType() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -993,7 +976,7 @@ class SubscriptionTests: XCTestCase { db.publisher.emit(event: "String instead of email") - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( diff --git a/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift b/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift index 915331bd..e6ed44fb 100644 --- a/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift +++ b/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift @@ -47,7 +47,7 @@ class GraphQLSchemaTests: XCTestCase { ), ], interfaces: [interface], - isTypeOf: { _, _, _ -> Bool in + isTypeOf: { _, _ -> Bool in preconditionFailure("Should not be called") } ) @@ -81,7 +81,7 @@ class GraphQLSchemaTests: XCTestCase { ), ], interfaces: [interface], - isTypeOf: { _, _, _ -> Bool in + isTypeOf: { _, _ -> Bool in preconditionFailure("Should not be called") } ) @@ -110,7 +110,7 @@ class GraphQLSchemaTests: XCTestCase { ), ], interfaces: [interface], - isTypeOf: { _, _, _ -> Bool in + isTypeOf: { _, _ -> Bool in preconditionFailure("Should not be called") } ) diff --git a/Tests/GraphQLTests/TypeTests/IntrospectionTests.swift b/Tests/GraphQLTests/TypeTests/IntrospectionTests.swift index 0a990391..c85c5bf8 100644 --- a/Tests/GraphQLTests/TypeTests/IntrospectionTests.swift +++ b/Tests/GraphQLTests/TypeTests/IntrospectionTests.swift @@ -1,19 +1,8 @@ @testable import GraphQL -import NIO import XCTest class IntrospectionTests: XCTestCase { - private var eventLoopGroup: EventLoopGroup! - - override func setUp() { - eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } - - override func tearDown() { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - - func testDefaultValues() throws { + func testDefaultValues() async throws { let numEnum = try GraphQLEnumType( name: "Enum", values: [ @@ -110,7 +99,7 @@ class IntrospectionTests: XCTestCase { let schema = try GraphQLSchema(query: query, types: [inputObject, outputObject]) - let introspection = try graphql( + let introspection = try await graphql( schema: schema, request: """ query IntrospectionTypeQuery { @@ -133,9 +122,8 @@ class IntrospectionTests: XCTestCase { } } } - """, - eventLoopGroup: eventLoopGroup - ).wait() + """ + ) let queryType = try XCTUnwrap( introspection.data?["__schema"]["types"].array? diff --git a/Tests/GraphQLTests/TypeTests/ScalarTests.swift b/Tests/GraphQLTests/TypeTests/ScalarTests.swift index 59bc6e87..494c6b93 100644 --- a/Tests/GraphQLTests/TypeTests/ScalarTests.swift +++ b/Tests/GraphQLTests/TypeTests/ScalarTests.swift @@ -1,5 +1,4 @@ @testable import GraphQL -import NIO import XCTest class ScalarTests: XCTestCase { diff --git a/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift b/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift index b7a34a44..91429598 100644 --- a/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift +++ b/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift @@ -1,5 +1,4 @@ @testable import GraphQL -import NIO import XCTest class BuildASTSchemaTests: XCTestCase { @@ -12,7 +11,7 @@ class BuildASTSchemaTests: XCTestCase { return try printSchema(schema: buildSchema(source: sdl)) } - func testCanUseBuiltSchemaForLimitedExecution() throws { + func testCanUseBuiltSchemaForLimitedExecution() async throws { let schema = try buildASTSchema( documentAST: parse( source: """ @@ -23,12 +22,11 @@ class BuildASTSchemaTests: XCTestCase { ) ) - let result = try graphql( + let result = try await graphql( schema: schema, request: "{ str }", - rootValue: ["str": 123], - eventLoopGroup: MultiThreadedEventLoopGroup(numberOfThreads: 1) - ).wait() + rootValue: ["str": 123] + ) XCTAssertEqual( result, @@ -50,16 +48,15 @@ class BuildASTSchemaTests: XCTestCase { // ) // ) // -// let result = try graphql( +// let result = try await graphql( // schema: schema, // request: "{ add(x: 34, y: 55) }", // rootValue: [ // "add": { (x: Int, y: Int) in // return x + y // } -// ], -// eventLoopGroup: MultiThreadedEventLoopGroup(numberOfThreads: 1) -// ).wait() +// ] +// ) // // XCTAssertEqual( // result, diff --git a/Tests/GraphQLTests/UtilitiesTests/ExtendSchemaTests.swift b/Tests/GraphQLTests/UtilitiesTests/ExtendSchemaTests.swift index 6c3a9625..fae4c70c 100644 --- a/Tests/GraphQLTests/UtilitiesTests/ExtendSchemaTests.swift +++ b/Tests/GraphQLTests/UtilitiesTests/ExtendSchemaTests.swift @@ -1,18 +1,7 @@ @testable import GraphQL -import NIO import XCTest class ExtendSchemaTests: XCTestCase { - private var eventLoopGroup: EventLoopGroup! - - override func setUp() { - eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } - - override func tearDown() { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - func schemaChanges( _ schema: GraphQLSchema, _ extendedSchema: GraphQLSchema @@ -46,7 +35,7 @@ class ExtendSchemaTests: XCTestCase { ) } - func testCanBeUsedForLimitedExecution() throws { + func testCanBeUsedForLimitedExecution() async throws { let schema = try buildSchema(source: "type Query") let extendAST = try parse(source: """ extend type Query { @@ -54,12 +43,11 @@ class ExtendSchemaTests: XCTestCase { } """) let extendedSchema = try extendSchema(schema: schema, documentAST: extendAST) - let result = try graphql( + let result = try await graphql( schema: extendedSchema, request: "{ newField }", - rootValue: ["newField": 123], - eventLoopGroup: eventLoopGroup - ).wait() + rootValue: ["newField": 123] + ) XCTAssertEqual( result, .init(data: ["newField": "123"]) diff --git a/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift b/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift index 2350c285..d0571f94 100644 --- a/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift +++ b/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift @@ -15,7 +15,7 @@ let ValidationExampleBeing = try! GraphQLInterfaceType( } ), ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -32,7 +32,7 @@ let ValidationExampleMammal = try! GraphQLInterfaceType( "father": GraphQLField(type: ValidationExampleMammal), ] }, - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -53,7 +53,7 @@ let ValidationExamplePet = try! GraphQLInterfaceType( } ), ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -78,7 +78,7 @@ let ValidationExampleCanine = try! GraphQLInterfaceType( type: ValidationExampleMammal ), ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -263,7 +263,7 @@ let ValidationExampleCat = try! GraphQLObjectType( // union CatOrDog = Cat | Dog let ValidationExampleCatOrDog = try! GraphQLUnionType( name: "CatOrDog", - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" }, types: [ValidationExampleCat, ValidationExampleDog] @@ -277,7 +277,7 @@ let ValidationExampleIntelligent = try! GraphQLInterfaceType( fields: [ "iq": GraphQLField(type: GraphQLInt), ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -293,7 +293,7 @@ let ValidationExampleSentient = try! GraphQLInterfaceType( return nil }, ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -363,7 +363,7 @@ let ValidationExampleCatCommand = try! GraphQLEnumType( // union DogOrHuman = Dog | Human let ValidationExampleDogOrHuman = try! GraphQLUnionType( name: "DogOrHuman", - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" }, types: [ValidationExampleDog, ValidationExampleHuman] @@ -372,7 +372,7 @@ let ValidationExampleDogOrHuman = try! GraphQLUnionType( // union HumanOrAlien = Human | Alien let ValidationExampleHumanOrAlien = try! GraphQLUnionType( name: "HumanOrAlien", - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" }, types: [ValidationExampleHuman, ValidationExampleAlien] From d112be0f913ed6702f596ee23878b576ff8814ef Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 24 Jun 2025 14:46:39 -0600 Subject: [PATCH 2/8] chore: Removes unnecessary @available --- Sources/GraphQL/Map/AnyCoder.swift | 4 ---- Sources/GraphQL/Map/GraphQLJSONEncoder.swift | 3 --- Sources/GraphQL/Map/MapCoder.swift | 4 ---- Sources/GraphQL/Map/Number.swift | 4 ---- Sources/GraphQL/Subscription/EventStream.swift | 2 -- Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift | 1 - Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift | 1 - Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift | 1 - Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift | 1 - 9 files changed, 21 deletions(-) diff --git a/Sources/GraphQL/Map/AnyCoder.swift b/Sources/GraphQL/Map/AnyCoder.swift index dcab5052..debf273d 100644 --- a/Sources/GraphQL/Map/AnyCoder.swift +++ b/Sources/GraphQL/Map/AnyCoder.swift @@ -41,7 +41,6 @@ open class AnyEncoder { public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0) /// Produce Any with dictionary keys sorted in lexicographic order. - @available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) } @@ -57,7 +56,6 @@ open class AnyEncoder { case millisecondsSince1970 /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Encode the `Date` as a string formatted by the given formatter. @@ -1103,7 +1101,6 @@ open class AnyDecoder { case millisecondsSince1970 /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Decode the `Date` as a string parsed by the given formatter. @@ -3258,7 +3255,6 @@ private struct _AnyKey: CodingKey { //===----------------------------------------------------------------------===// // NOTE: This value is implicitly lazy and _must_ be lazy. We're compiled against the latest SDK (w/ ISO8601DateFormatter), but linked against whichever Foundation the user has. ISO8601DateFormatter might not exist, so we better not hit this code path on an older OS. -@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) private var _iso8601Formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime diff --git a/Sources/GraphQL/Map/GraphQLJSONEncoder.swift b/Sources/GraphQL/Map/GraphQLJSONEncoder.swift index a938ee4f..cb6029a7 100644 --- a/Sources/GraphQL/Map/GraphQLJSONEncoder.swift +++ b/Sources/GraphQL/Map/GraphQLJSONEncoder.swift @@ -39,7 +39,6 @@ open class GraphQLJSONEncoder { public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0) /// Produce JSON with dictionary keys sorted in lexicographic order. - @available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) /// By default slashes get escaped ("/" → "\/", "http://apple.com/" → "http:\/\/apple.com\/") @@ -61,7 +60,6 @@ open class GraphQLJSONEncoder { case millisecondsSince1970 /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Encode the `Date` as a string formatted by the given formatter. @@ -1298,7 +1296,6 @@ internal struct _JSONKey: CodingKey { //===----------------------------------------------------------------------===// // NOTE: This value is implicitly lazy and _must_ be lazy. We're compiled against the latest SDK (w/ ISO8601DateFormatter), but linked against whichever Foundation the user has. ISO8601DateFormatter might not exist, so we better not hit this code path on an older OS. -@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) private var _iso8601Formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime diff --git a/Sources/GraphQL/Map/MapCoder.swift b/Sources/GraphQL/Map/MapCoder.swift index aabcac69..cc334dda 100644 --- a/Sources/GraphQL/Map/MapCoder.swift +++ b/Sources/GraphQL/Map/MapCoder.swift @@ -49,7 +49,6 @@ open class MapEncoder { public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0) /// Produce Map with dictionary keys sorted in lexicographic order. - @available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) } @@ -65,7 +64,6 @@ open class MapEncoder { case millisecondsSince1970 /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Encode the `Date` as a string formatted by the given formatter. @@ -1111,7 +1109,6 @@ open class MapDecoder { case millisecondsSince1970 /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Decode the `Date` as a string parsed by the given formatter. @@ -3266,7 +3263,6 @@ private struct _MapKey: CodingKey { //===----------------------------------------------------------------------===// // NOTE: This value is implicitly lazy and _must_ be lazy. We're compiled against the latest SDK (w/ ISO8601DateFormatter), but linked against whichever Foundation the user has. ISO8601DateFormatter might not exist, so we better not hit this code path on an older OS. -@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) private var _iso8601Formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime diff --git a/Sources/GraphQL/Map/Number.swift b/Sources/GraphQL/Map/Number.swift index f711cb27..d0459c8e 100644 --- a/Sources/GraphQL/Map/Number.swift +++ b/Sources/GraphQL/Map/Number.swift @@ -35,13 +35,11 @@ public struct Number: Sendable { storageType = .bool } - @available(OSX 10.5, *) public init(_ value: Int) { _number = NSNumber(value: value) storageType = .int } - @available(OSX 10.5, *) public init(_ value: UInt) { _number = NSNumber(value: value) storageType = .int @@ -101,12 +99,10 @@ public struct Number: Sendable { return _number.boolValue } - @available(OSX 10.5, *) public var intValue: Int { return _number.intValue } - @available(OSX 10.5, *) public var uintValue: UInt { return _number.uintValue } diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift index 2f78093c..aad15123 100644 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ b/Sources/GraphQL/Subscription/EventStream.swift @@ -9,7 +9,6 @@ open class EventStream { } /// Event stream that wraps an `AsyncThrowingStream` from Swift's standard concurrency system. -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) public class ConcurrentEventStream: EventStream { public let stream: AsyncThrowingStream @@ -28,7 +27,6 @@ public class ConcurrentEventStream: EventStream { } } -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) extension AsyncThrowingStream { func mapStream(_ closure: @escaping (Element) async throws -> To) -> AsyncThrowingStream { diff --git a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift index c4da2977..3131c600 100644 --- a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift @@ -48,7 +48,6 @@ class HelloWorldTests: XCTestCase { XCTAssertEqual(result, expected) } - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) func testHelloAsync() async throws { let query = "{ hello }" let expected = GraphQLResult(data: ["hello": "world"]) diff --git a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift index 8a742378..e33f1ba4 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift @@ -1,7 +1,6 @@ import GraphQL /// A very simple publish/subscriber used for testing -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) class SimplePubSub { private var subscribers: [Subscriber] diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index b535fc43..ad4cf3a8 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -88,7 +88,6 @@ let EmailQueryType = try! GraphQLObjectType( // MARK: Test Helpers -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) class EmailDb { var emails: [Email] let publisher: SimplePubSub diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index fc52ffea..f429a276 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -2,7 +2,6 @@ import GraphQL import XCTest /// This follows the graphql-js testing, with deviations where noted. -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) class SubscriptionTests: XCTestCase { let timeoutDuration = 0.5 // in seconds From 9ec313a8730ecd8b2d7c3768ea0d0791d018b786 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 24 Jun 2025 16:09:57 -0600 Subject: [PATCH 3/8] feat!: Removes EventStream, replacing with AsyncThrowingStream --- MIGRATION.md | 4 + README.md | 8 +- Sources/GraphQL/GraphQL.swift | 11 +- .../GraphQL/Subscription/EventStream.swift | 73 --------- Sources/GraphQL/Subscription/Subscribe.swift | 65 +++++--- .../SubscriptionTests/SimplePubSub.swift | 5 +- .../SubscriptionSchema.swift | 19 ++- .../SubscriptionTests/SubscriptionTests.swift | 153 ++++-------------- 8 files changed, 94 insertions(+), 244 deletions(-) delete mode 100644 Sources/GraphQL/Subscription/EventStream.swift diff --git a/MIGRATION.md b/MIGRATION.md index 8ef81076..1795c5ad 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -16,6 +16,10 @@ The documentation here will be very helpful in the conversion: https://www.swift This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. +### EventStream removal + +The `EventStream` abstraction used to provide pre-concurrency subscription support has been removed. This means that `graphqlSubscribe(...).stream` will now be an `AsyncThrowingStream` type, instead of an `EventStream` type, and that downcasting to `ConcurrentEventStream` is no longer necessary. + ## 2 to 3 ### TypeReference removal diff --git a/README.md b/README.md index 06b7ae2f..8f94cb5b 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ let schema = try GraphQLSchema( return eventResult }, subscribe: { _, _, _, _, _ in // Defines how to construct the event stream - let asyncStream = AsyncThrowingStream { continuation in + return AsyncThrowingStream { continuation in let timer = Timer.scheduledTimer( withTimeInterval: 3, repeats: true, @@ -83,7 +83,6 @@ let schema = try GraphQLSchema( continuation.yield("world") // Emits "world" every 3 seconds } } - return ConcurrentEventStream(asyncStream) } ) ] @@ -97,8 +96,6 @@ To execute a subscription use the `graphqlSubscribe` function: let subscriptionResult = try await graphqlSubscribe( schema: schema, ) -// Must downcast from EventStream to concrete type to use in 'for await' loop below -let concurrentStream = subscriptionResult.stream! as! ConcurrentEventStream for try await result in concurrentStream.stream { print(result) } @@ -110,9 +107,6 @@ The code above will print the following JSON every 3 seconds: { "hello": "world" } ``` -The example above assumes that your environment has access to Swift Concurrency. If that is not the case, try using -[GraphQLRxSwift](https://github.com/GraphQLSwift/GraphQLRxSwift) - ## Encoding Results If you encode a `GraphQLResult` with an ordinary `JSONEncoder`, there are no guarantees that the field order will match the query, diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 0488bb44..24f26eb4 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -44,19 +44,18 @@ public struct GraphQLResult: Equatable, Codable, Sendable, CustomStringConvertib /// SubscriptionResult wraps the observable and error data returned by the subscribe request. public struct SubscriptionResult { - public let stream: SubscriptionEventStream? + public let stream: AsyncThrowingStream? public let errors: [GraphQLError] - public init(stream: SubscriptionEventStream? = nil, errors: [GraphQLError] = []) { + public init( + stream: AsyncThrowingStream? = 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 SubscriptionEventStream = EventStream - /// 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/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift deleted file mode 100644 index aad15123..00000000 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ /dev/null @@ -1,73 +0,0 @@ -/// Abstract event stream class - Should be overridden for actual implementations -open class EventStream { - public init() {} - /// Template method for mapping an event stream to a new generic type - MUST be overridden by - /// implementing types. - open func map(_: @escaping (Element) async throws -> To) -> EventStream { - fatalError("This function should be overridden by implementing classes") - } -} - -/// Event stream that wraps an `AsyncThrowingStream` from Swift's standard concurrency system. -public class ConcurrentEventStream: EventStream { - public let stream: AsyncThrowingStream - - public init(_ stream: AsyncThrowingStream) { - self.stream = stream - } - - /// Performs the closure on each event in the current stream and returns a stream of the - /// results. - /// - Parameter closure: The closure to apply to each event in the stream - /// - Returns: A stream of the results - override open func map(_ closure: @escaping (Element) async throws -> To) - -> ConcurrentEventStream { - let newStream = stream.mapStream(closure) - return ConcurrentEventStream(newStream) - } -} - -extension AsyncThrowingStream { - func mapStream(_ closure: @escaping (Element) async throws -> To) - -> AsyncThrowingStream { - return AsyncThrowingStream { continuation in - let task = Task { - do { - for try await event in self { - let newEvent = try await closure(event) - continuation.yield(newEvent) - } - continuation.finish() - } catch { - continuation.finish(throwing: error) - } - } - - continuation.onTermination = { @Sendable reason in - task.cancel() - } - } - } - - func filterStream(_ isIncluded: @escaping (Element) throws -> Bool) - -> AsyncThrowingStream { - return AsyncThrowingStream { continuation in - let task = Task { - do { - for try await event in self { - if try isIncluded(event) { - continuation.yield(event) - } - } - continuation.finish() - } catch { - continuation.finish(throwing: error) - } - } - - continuation.onTermination = { @Sendable _ in - task.cancel() - } - } - } -} diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 30dd61cb..ac9f44db 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -46,25 +46,42 @@ func subscribe( ) if let sourceStream = sourceResult.stream { - let subscriptionStream = sourceStream.map { eventPayload -> GraphQLResult in - // 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. - try await execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - documentAST: documentAST, - rootValue: eventPayload, - context: context, - variableValues: variableValues, - operationName: operationName - ) + // We must create a new AsyncSequence because AsyncSequence.map requires a concrete type + // (which we cannot know), + // and we need the result to be a concrete type. + let subscriptionStream = AsyncThrowingStream { continuation in + let task = Task { + do { + for try await eventPayload in sourceStream { + // 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 newEvent = try await execute( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + documentAST: documentAST, + rootValue: eventPayload, + context: context, + variableValues: variableValues, + operationName: operationName + ) + continuation.yield(newEvent) + } + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + + continuation.onTermination = { @Sendable reason in + task.cancel() + } } return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) } else { @@ -151,7 +168,7 @@ func createSourceEventStream( } func executeSubscription( - context: ExecutionContext, + context: ExecutionContext ) async throws -> SourceEventStreamResult { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) @@ -245,7 +262,7 @@ func executeSubscription( return SourceEventStreamResult(errors: context.errors) } else if let error = resolved as? GraphQLError { return SourceEventStreamResult(errors: [error]) - } else if let stream = resolved as? EventStream { + } else if let stream = resolved as? any AsyncSequence { return SourceEventStreamResult(stream: stream) } else if resolved == nil { return SourceEventStreamResult(errors: [ @@ -255,7 +272,7 @@ func executeSubscription( let resolvedObj = resolved as AnyObject return SourceEventStreamResult(errors: [ GraphQLError( - message: "Subscription field resolver must return EventStream. Received: '\(resolvedObj)'" + message: "Subscription field resolver must return an AsyncSequence. Received: '\(resolvedObj)'" ), ]) } @@ -266,10 +283,10 @@ func executeSubscription( // checking. Normal resolvers for subscription fields should handle type casting, same as resolvers // for query fields. struct SourceEventStreamResult { - public let stream: EventStream? + public let stream: (any AsyncSequence)? public let errors: [GraphQLError] - public init(stream: EventStream? = nil, errors: [GraphQLError] = []) { + public init(stream: (any AsyncSequence)? = nil, errors: [GraphQLError] = []) { self.stream = stream self.errors = errors } diff --git a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift index e33f1ba4..fb198bfa 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift @@ -20,8 +20,8 @@ class SimplePubSub { } } - func subscribe() -> ConcurrentEventStream { - let asyncStream = AsyncThrowingStream { continuation in + func subscribe() -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in let subscriber = Subscriber( callback: { newValue in continuation.yield(newValue) @@ -32,7 +32,6 @@ class SimplePubSub { ) subscribers.append(subscriber) } - return ConcurrentEventStream(asyncStream) } } diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index ad4cf3a8..53d4bab1 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -129,15 +129,14 @@ class EmailDb { }, subscribe: { _, args, _, _ throws -> Any? in let priority = args["priority"].int ?? 0 - let filtered = self.publisher.subscribe().stream - .filterStream { emailAny throws in - if let email = emailAny as? Email { - return email.priority >= priority - } else { - return true - } + let filtered = self.publisher.subscribe().filter { emailAny throws in + if let email = emailAny as? Email { + return email.priority >= priority + } else { + return true } - return ConcurrentEventStream(filtered) + } + return filtered } ) } @@ -146,7 +145,7 @@ class EmailDb { func subscription( query: String, variableValues: [String: Map] = [:] - ) async throws -> SubscriptionEventStream { + ) async throws -> AsyncThrowingStream { return try await createSubscription( schema: defaultSchema(), query: query, @@ -186,7 +185,7 @@ func createSubscription( schema: GraphQLSchema, query: String, variableValues: [String: Map] = [:] -) async throws -> SubscriptionEventStream { +) async throws -> AsyncThrowingStream { let result = try await graphqlSubscribe( queryStrategy: SerialFieldExecutionStrategy(), mutationStrategy: SerialFieldExecutionStrategy(), diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index f429a276..8711991b 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -30,15 +30,11 @@ class SubscriptionTests: XCTestCase { schema: schema, request: query ) - guard let subscription = subscriptionResult.stream else { + guard let stream = subscriptionResult.stream else { XCTFail(subscriptionResult.errors.description) return } - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() db.trigger(email: Email( from: "yuzhi@graphql.org", @@ -122,7 +118,7 @@ class SubscriptionTests: XCTestCase { ] ) ) - let subscription = try await createSubscription(schema: schema, query: """ + let stream = try await createSubscription(schema: schema, query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -136,11 +132,7 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() db.trigger(email: Email( from: "yuzhi@graphql.org", @@ -298,7 +290,7 @@ class SubscriptionTests: XCTestCase { } XCTAssertEqual( graphQLError.message, - "Subscription field resolver must return EventStream. Received: 'test'" + "Subscription field resolver must return an AsyncSequence. Received: 'test'" ) } } @@ -394,7 +386,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload for a single subscriber' func testSingleSubscriber() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -408,11 +400,7 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() db.trigger(email: Email( from: "yuzhi@graphql.org", @@ -443,7 +431,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload for multiple subscribe in same subscription' func testMultipleSubscribers() async throws { let db = EmailDb() - let subscription1 = try await db.subscription(query: """ + let stream1 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -457,12 +445,8 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream1 = subscription1 as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - let subscription2 = try await db.subscription(query: """ + let stream2 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -476,13 +460,9 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream2 = subscription2 as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator1 = stream1.stream.makeAsyncIterator() - var iterator2 = stream2.stream.makeAsyncIterator() + var iterator1 = stream1.makeAsyncIterator() + var iterator2 = stream2.makeAsyncIterator() db.trigger(email: Email( from: "yuzhi@graphql.org", @@ -514,7 +494,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload per subscription event' func testPayloadPerEvent() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -528,11 +508,7 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() // A new email arrives! db.trigger(email: Email( @@ -587,7 +563,7 @@ class SubscriptionTests: XCTestCase { /// This is not in the graphql-js tests. func testArguments() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 5) { importantEmail(priority: $priority) { email { @@ -601,21 +577,8 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - - var results = [GraphQLResult]() - var expectation = XCTestExpectation() - - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - let keepForNow = stream.map { result in - results.append(result) - expectation.fulfill() - } - + var iterator = stream.makeAsyncIterator() + var results = [GraphQLResult?]() var expected = [GraphQLResult]() db.trigger(email: Email( @@ -639,12 +602,10 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) // Low priority email shouldn't trigger an event - expectation = XCTestExpectation() - expectation.isInverted = true db.trigger(email: Email( from: "hyo@graphql.org", subject: "Not Important", @@ -652,11 +613,9 @@ class SubscriptionTests: XCTestCase { unread: true, priority: 2 )) - wait(for: [expectation], timeout: timeoutDuration) XCTAssertEqual(results, expected) // Higher priority one should trigger again - expectation = XCTestExpectation() db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", @@ -678,18 +637,14 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) - - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - _ = keepForNow } /// 'should not trigger when subscription is already done' func testNoTriggerAfterDone() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -703,19 +658,8 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - - var results = [GraphQLResult]() - var expectation = XCTestExpectation() - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - let keepForNow = stream.map { result in - results.append(result) - expectation.fulfill() - } + var iterator = stream.makeAsyncIterator() + var results = [GraphQLResult?]() var expected = [GraphQLResult]() db.trigger(email: Email( @@ -738,28 +682,20 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) db.stop() // This should not trigger an event. - expectation = XCTestExpectation() - expectation.isInverted = true db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", unread: true )) - // Ensure that the current result was the one before the db was stopped - wait(for: [expectation], timeout: timeoutDuration) XCTAssertEqual(results, expected) - - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - _ = keepForNow } /// 'should not trigger when subscription is thrown' @@ -768,7 +704,7 @@ class SubscriptionTests: XCTestCase { /// 'event order is correct for multiple publishes' func testOrderCorrectForMultiplePublishes() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -782,11 +718,7 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() db.trigger(email: Email( from: "yuzhi@graphql.org", @@ -860,7 +792,7 @@ class SubscriptionTests: XCTestCase { } ) - let subscription = try await createSubscription(schema: schema, query: """ + let stream = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -869,19 +801,8 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - - var results = [GraphQLResult]() - var expectation = XCTestExpectation() - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - let keepForNow = stream.map { result in - results.append(result) - expectation.fulfill() - } + var iterator = stream.makeAsyncIterator() + var results = [GraphQLResult?]() var expected = [GraphQLResult]() db.trigger(email: Email( @@ -899,10 +820,9 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) - expectation = XCTestExpectation() // An error in execution is presented as such. db.trigger(email: Email( from: "yuzhi@graphql.org", @@ -918,10 +838,9 @@ class SubscriptionTests: XCTestCase { ] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) - expectation = XCTestExpectation() // However that does not close the response event stream. Subsequent events are still // executed. db.trigger(email: Email( @@ -939,12 +858,8 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) - - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - _ = keepForNow } /// 'should pass through error thrown in source event stream' @@ -953,7 +868,7 @@ class SubscriptionTests: XCTestCase { /// Test incorrect emitted type errors func testErrorWrongEmitType() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -967,11 +882,7 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() db.publisher.emit(event: "String instead of email") From 9b4a6f27fee56f8f4a5188dd1ee20572a6a91127 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 26 Jun 2025 18:22:10 -0600 Subject: [PATCH 4/8] feat!: Removes Instrumentation The intent is to replace it with swift-distributed-tracing integration. --- MIGRATION.md | 4 + Sources/GraphQL/Execution/Execute.swift | 35 ---- Sources/GraphQL/GraphQL.swift | 18 +- Sources/GraphQL/GraphQLRequest.swift | 1 - .../DispatchQueueInstrumentationWrapper.swift | 136 ------------ .../Instrumentation/Instrumentation.swift | 121 ----------- Sources/GraphQL/Language/Parser.swift | 20 -- Sources/GraphQL/Subscription/Subscribe.swift | 21 -- Sources/GraphQL/Validation/Validate.swift | 15 +- .../InstrumentationTests.swift | 194 ------------------ .../SubscriptionSchema.swift | 1 - 11 files changed, 7 insertions(+), 559 deletions(-) delete mode 100644 Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift delete mode 100644 Sources/GraphQL/Instrumentation/Instrumentation.swift delete mode 100644 Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift diff --git a/MIGRATION.md b/MIGRATION.md index 1795c5ad..325fc0cd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -20,6 +20,10 @@ This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. The `EventStream` abstraction used to provide pre-concurrency subscription support has been removed. This means that `graphqlSubscribe(...).stream` will now be an `AsyncThrowingStream` type, instead of an `EventStream` type, and that downcasting to `ConcurrentEventStream` is no longer necessary. +### Instrumentation removal + +The `Instrumentation` type has been removed, with anticipated support for tracing using [`swift-distributed-tracing`](https://github.com/apple/swift-distributed-tracing). `instrumentation` arguments must be removed from `graphql` and `graphqlSubscribe` calls. + ## 2 to 3 ### TypeReference removal diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 005f3359..403b65ab 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -31,7 +31,6 @@ public final class ExecutionContext { let queryStrategy: QueryFieldExecutionStrategy let mutationStrategy: MutationFieldExecutionStrategy let subscriptionStrategy: SubscriptionFieldExecutionStrategy - let instrumentation: Instrumentation public let schema: GraphQLSchema public let fragments: [String: FragmentDefinition] public let rootValue: Any @@ -54,7 +53,6 @@ public final class ExecutionContext { queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, fragments: [String: FragmentDefinition], rootValue: Any, @@ -66,7 +64,6 @@ public final class ExecutionContext { self.queryStrategy = queryStrategy self.mutationStrategy = mutationStrategy self.subscriptionStrategy = subscriptionStrategy - self.instrumentation = instrumentation self.schema = schema self.fragments = fragments self.rootValue = rootValue @@ -180,7 +177,6 @@ func execute( queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, rootValue: Any, @@ -188,7 +184,6 @@ func execute( variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> GraphQLResult { - let executeStarted = instrumentation.now let buildContext: ExecutionContext do { @@ -198,7 +193,6 @@ func execute( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -207,20 +201,6 @@ func execute( operationName: operationName ) } catch let error as GraphQLError { - instrumentation.operationExecution( - processId: processId(), - threadId: threadId(), - started: executeStarted, - finished: instrumentation.now, - schema: schema, - document: documentAST, - rootValue: rootValue, - variableValues: variableValues, - operation: nil, - errors: [error], - result: nil - ) - return GraphQLResult(errors: [error]) } catch { return GraphQLResult(errors: [GraphQLError(error)]) @@ -264,7 +244,6 @@ func buildExecutionContext( queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, rootValue: Any, @@ -318,7 +297,6 @@ func buildExecutionContext( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, fragments: fragments, rootValue: rootValue, @@ -641,8 +619,6 @@ public func resolveField( variableValues: exeContext.variableValues ) -// let resolveFieldStarted = exeContext.instrumentation.now - // Get the resolve func, regardless of if its result is normal // or abrupt (error). let result = await resolveOrError( @@ -653,17 +629,6 @@ public func resolveField( info: info ) -// exeContext.instrumentation.fieldResolution( -// processId: processId(), -// threadId: threadId(), -// started: resolveFieldStarted, -// finished: exeContext.instrumentation.now, -// source: source, -// args: args, -// info: info, -// result: result -// ) - return try await completeValueCatchingError( exeContext: exeContext, returnType: returnType, diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 24f26eb4..36bc3456 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -67,8 +67,6 @@ public struct SubscriptionResult { /// - 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 @@ -93,7 +91,6 @@ public func graphql( queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, validationRules: [(ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, @@ -103,9 +100,8 @@ public func graphql( operationName: String? = nil ) async throws -> GraphQLResult { let source = Source(body: request, name: "GraphQL request") - let documentAST = try parse(instrumentation: instrumentation, source: source) + let documentAST = try parse(source: source) let validationErrors = validate( - instrumentation: instrumentation, schema: schema, ast: documentAST, rules: validationRules @@ -119,7 +115,6 @@ public func graphql( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -135,8 +130,6 @@ public func graphql( /// - 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 queryRetrieval: The PersistedQueryRetrieval instance to use for looking up /// queries /// - parameter queryId: The id of the query to execute @@ -160,7 +153,6 @@ public func graphql( queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, queryRetrieval: Retrieval, queryId: Retrieval.Id, rootValue: Any = (), @@ -180,7 +172,6 @@ public func graphql( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -202,8 +193,6 @@ public func graphql( /// - 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 @@ -232,7 +221,6 @@ public func graphqlSubscribe( queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, validationRules: [(ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, @@ -242,9 +230,8 @@ public func graphqlSubscribe( operationName: String? = nil ) async throws -> SubscriptionResult { let source = Source(body: request, name: "GraphQL Subscription request") - let documentAST = try parse(instrumentation: instrumentation, source: source) + let documentAST = try parse(source: source) let validationErrors = validate( - instrumentation: instrumentation, schema: schema, ast: documentAST, rules: validationRules @@ -258,7 +245,6 @@ public func graphqlSubscribe( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, diff --git a/Sources/GraphQL/GraphQLRequest.swift b/Sources/GraphQL/GraphQLRequest.swift index bfcca8ff..035ae174 100644 --- a/Sources/GraphQL/GraphQLRequest.swift +++ b/Sources/GraphQL/GraphQLRequest.swift @@ -37,7 +37,6 @@ public struct GraphQLRequest: Equatable, Codable { /// - Returns: The operation type performed by the request public func operationType() throws -> OperationType { let documentAST = try GraphQL.parse( - instrumentation: NoOpInstrumentation, source: Source(body: query, name: "GraphQL request") ) let firstOperation = documentAST.definitions.compactMap { $0 as? OperationDefinition }.first diff --git a/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift b/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift deleted file mode 100644 index f58ce3f5..00000000 --- a/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Dispatch - -/// Proxies calls through to another `Instrumentation` instance via a DispatchQueue -/// -/// Has two primary use cases: -/// 1. Allows a non thread safe Instrumentation implementation to be used along side a multithreaded -/// execution strategy -/// 2. Allows slow or heavy instrumentation processing to happen outside of the current query -/// execution -public class DispatchQueueInstrumentationWrapper: Instrumentation { - let instrumentation: Instrumentation - let dispatchQueue: DispatchQueue - let dispatchGroup: DispatchGroup? - - public init( - _ instrumentation: Instrumentation, - label: String = "GraphQL instrumentation wrapper", - qos: DispatchQoS = .utility, - attributes: DispatchQueue.Attributes = [], - dispatchGroup: DispatchGroup? = nil - ) { - self.instrumentation = instrumentation - dispatchQueue = DispatchQueue(label: label, qos: qos, attributes: attributes) - self.dispatchGroup = dispatchGroup - } - - public init( - _ instrumentation: Instrumentation, - dispatchQueue: DispatchQueue, - dispatchGroup: DispatchGroup? = nil - ) { - self.instrumentation = instrumentation - self.dispatchQueue = dispatchQueue - self.dispatchGroup = dispatchGroup - } - - public var now: DispatchTime { - return instrumentation.now - } - - public func queryParsing( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Source, - result: Result - ) { - dispatchQueue.async(group: dispatchGroup) { - self.instrumentation.queryParsing( - processId: processId, - threadId: threadId, - started: started, - finished: finished, - source: source, - result: result - ) - } - } - - public func queryValidation( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - schema: GraphQLSchema, - document: Document, - errors: [GraphQLError] - ) { - dispatchQueue.async(group: dispatchGroup) { - self.instrumentation.queryValidation( - processId: processId, - threadId: threadId, - started: started, - finished: finished, - schema: schema, - document: document, - errors: errors - ) - } - } - - public func operationExecution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - schema: GraphQLSchema, - document: Document, - rootValue: Any, - variableValues: [String: Map], - operation: OperationDefinition?, - errors: [GraphQLError], - result: Map - ) { - dispatchQueue.async(group: dispatchGroup) { - self.instrumentation.operationExecution( - processId: processId, - threadId: threadId, - started: started, - finished: finished, - schema: schema, - document: document, - rootValue: rootValue, - variableValues: variableValues, - operation: operation, - errors: errors, - result: result - ) - } - } - - public func fieldResolution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Any, - args: Map, - info: GraphQLResolveInfo, - result: Result - ) { - dispatchQueue.async(group: dispatchGroup) { - self.instrumentation.fieldResolution( - processId: processId, - threadId: threadId, - started: started, - finished: finished, - source: source, - args: args, - info: info, - result: result - ) - } - } -} diff --git a/Sources/GraphQL/Instrumentation/Instrumentation.swift b/Sources/GraphQL/Instrumentation/Instrumentation.swift deleted file mode 100644 index 90fd04b0..00000000 --- a/Sources/GraphQL/Instrumentation/Instrumentation.swift +++ /dev/null @@ -1,121 +0,0 @@ -import Dispatch -import Foundation - -/// Provides the capability to instrument the execution steps of a GraphQL query. -/// -/// A working implementation of `now` is also provided by default. -public protocol Instrumentation { - var now: DispatchTime { get } - - func queryParsing( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Source, - result: Result - ) - - func queryValidation( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - schema: GraphQLSchema, - document: Document, - errors: [GraphQLError] - ) - - func operationExecution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - schema: GraphQLSchema, - document: Document, - rootValue: Any, - variableValues: [String: Map], - operation: OperationDefinition?, - errors: [GraphQLError], - result: Map - ) - - func fieldResolution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Any, - args: Map, - info: GraphQLResolveInfo, - result: Result - ) -} - -public extension Instrumentation { - var now: DispatchTime { - return DispatchTime.now() - } -} - -func threadId() -> Int { - #if os(Linux) || os(Android) - return Int(pthread_self()) - #else - return Int(pthread_mach_thread_np(pthread_self())) - #endif -} - -func processId() -> Int { - return Int(getpid()) -} - -/// Does nothing -public let NoOpInstrumentation: Instrumentation = noOpInstrumentation() - -struct noOpInstrumentation: Instrumentation { - public let now = DispatchTime(uptimeNanoseconds: 0) - public func queryParsing( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - source _: Source, - result _: Result - ) {} - - public func queryValidation( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - schema _: GraphQLSchema, - document _: Document, - errors _: [GraphQLError] - ) {} - - public func operationExecution( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - schema _: GraphQLSchema, - document _: Document, - rootValue _: Any, - variableValues _: [String: Map], - operation _: OperationDefinition?, - errors _: [GraphQLError], - result _: Map - ) {} - - public func fieldResolution( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - source _: Any, - args _: Map, - info _: GraphQLResolveInfo, - result _: Result - ) {} -} diff --git a/Sources/GraphQL/Language/Parser.swift b/Sources/GraphQL/Language/Parser.swift index 87f9432e..90ef4e29 100644 --- a/Sources/GraphQL/Language/Parser.swift +++ b/Sources/GraphQL/Language/Parser.swift @@ -3,12 +3,10 @@ * Throws GraphQLError if a syntax error is encountered. */ public func parse( - instrumentation: Instrumentation = NoOpInstrumentation, source: String, noLocation: Bool = false ) throws -> Document { return try parse( - instrumentation: instrumentation, source: Source(body: source), noLocation: noLocation ) @@ -19,32 +17,14 @@ public func parse( * Throws GraphQLError if a syntax error is encountered. */ public func parse( - instrumentation: Instrumentation = NoOpInstrumentation, source: Source, noLocation: Bool = false ) throws -> Document { - let started = instrumentation.now do { let lexer = createLexer(source: source, noLocation: noLocation) let document = try parseDocument(lexer: lexer) - instrumentation.queryParsing( - processId: processId(), - threadId: threadId(), - started: started, - finished: instrumentation.now, - source: source, - result: .success(document) - ) return document } catch let error as GraphQLError { - instrumentation.queryParsing( - processId: processId(), - threadId: threadId(), - started: started, - finished: instrumentation.now, - source: source, - result: .failure(error) - ) throw error } } diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index ac9f44db..82f7b842 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -24,7 +24,6 @@ func subscribe( queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, rootValue: Any, @@ -36,7 +35,6 @@ func subscribe( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -63,7 +61,6 @@ func subscribe( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: eventPayload, @@ -120,7 +117,6 @@ func createSourceEventStream( queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, rootValue: Any, @@ -128,8 +124,6 @@ func createSourceEventStream( variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> SourceEventStreamResult { - let executeStarted = instrumentation.now - do { // If a valid context cannot be created due to incorrect arguments, // this will throw an error. @@ -137,7 +131,6 @@ func createSourceEventStream( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -147,20 +140,6 @@ func createSourceEventStream( ) return try await executeSubscription(context: exeContext) } catch let error as GraphQLError { - instrumentation.operationExecution( - processId: processId(), - threadId: threadId(), - started: executeStarted, - finished: instrumentation.now, - schema: schema, - document: documentAST, - rootValue: rootValue, - variableValues: variableValues, - operation: nil, - errors: [error], - result: nil - ) - return SourceEventStreamResult(errors: [error]) } catch { return SourceEventStreamResult(errors: [GraphQLError(error)]) diff --git a/Sources/GraphQL/Validation/Validate.swift b/Sources/GraphQL/Validation/Validate.swift index 362d4ad4..dfa5f651 100644 --- a/Sources/GraphQL/Validation/Validate.swift +++ b/Sources/GraphQL/Validation/Validate.swift @@ -4,17 +4,15 @@ /// an empty array if no errors were encountered and the document is valid. /// /// - Parameters: -/// - instrumentation: The instrumentation implementation to call during the parsing, validating, /// execution, and field resolution stages. /// - schema: The GraphQL type system to use when validating and executing a query. /// - ast: A GraphQL document representing the requested operation. /// - Returns: zero or more errors public func validate( - instrumentation: Instrumentation = NoOpInstrumentation, schema: GraphQLSchema, ast: Document ) -> [GraphQLError] { - return validate(instrumentation: instrumentation, schema: schema, ast: ast, rules: []) + return validate(schema: schema, ast: ast, rules: []) } /** @@ -31,24 +29,13 @@ public func validate( * GraphQLErrors, or Arrays of GraphQLErrors when invalid. */ public func validate( - instrumentation: Instrumentation = NoOpInstrumentation, schema: GraphQLSchema, ast: Document, rules: [(ValidationContext) -> Visitor] ) -> [GraphQLError] { - let started = instrumentation.now let typeInfo = TypeInfo(schema: schema) let rules = rules.isEmpty ? specifiedRules : rules let errors = visit(usingRules: rules, schema: schema, typeInfo: typeInfo, documentAST: ast) - instrumentation.queryValidation( - processId: processId(), - threadId: threadId(), - started: started, - finished: instrumentation.now, - schema: schema, - document: ast, - errors: errors - ) return errors } diff --git a/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift b/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift deleted file mode 100644 index 3583aa7f..00000000 --- a/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift +++ /dev/null @@ -1,194 +0,0 @@ -import Dispatch -import Foundation -import GraphQL -import XCTest - -class InstrumentationTests: XCTestCase, Instrumentation { - class MyRoot {} - class MyCtx {} - - var query = "query sayHello($name: String) { hello(name: $name) }" - var expectedResult: Map = [ - "data": [ - "hello": "bob", - ], - ] - var expectedThreadId = 0 - var expectedProcessId = 0 - var expectedRoot = MyRoot() - var expectedCtx = MyCtx() - var expectedOpVars: [String: Map] = ["name": "bob"] - var expectedOpName = "sayHello" - var queryParsingCalled = 0 - var queryValidationCalled = 0 - var operationExecutionCalled = 0 - var fieldResolutionCalled = 0 - - let schema = try! GraphQLSchema( - query: GraphQLObjectType( - name: "RootQueryType", - fields: [ - "hello": GraphQLField( - type: GraphQLString, - args: [ - "name": GraphQLArgument(type: GraphQLNonNull(GraphQLString)), - ], - resolve: { inputValue, _, _, _ in - print(type(of: inputValue)) - return nil - } -// resolve: { _, args, _, _ in return try! args["name"].asString() } - ), - ] - ) - ) - - override func setUp() { - expectedThreadId = 0 - expectedProcessId = 0 - queryParsingCalled = 0 - queryValidationCalled = 0 - operationExecutionCalled = 0 - fieldResolutionCalled = 0 - } - - func queryParsing( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - source _: Source, - result _: Result - ) { -// queryParsingCalled += 1 -// XCTAssertEqual(processId, expectedProcessId, "unexpected process id") -// XCTAssertEqual(threadId, expectedThreadId, "unexpected thread id") -// XCTAssertGreaterThan(finished, started) -// XCTAssertEqual(source.name, "GraphQL request") -// switch result { -// case .error(let e): -// XCTFail("unexpected error \(e)") -// case .result(let document): -// XCTAssertEqual(document.loc!.source.name, source.name) -// } -// XCTAssertEqual(source.name, "GraphQL request") - } - - func queryValidation( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - schema _: GraphQLSchema, - document _: Document, - errors _: [GraphQLError] - ) { - queryValidationCalled += 1 -// XCTAssertEqual(processId, expectedProcessId, "unexpected process id") -// XCTAssertEqual(threadId, expectedThreadId, "unexpected thread id") -// XCTAssertGreaterThan(finished, started) -// XCTAssertTrue(schema === self.schema) -// XCTAssertEqual(document.loc!.source.name, "GraphQL request") -// XCTAssertEqual(errors, []) - } - - func operationExecution( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - schema _: GraphQLSchema, - document _: Document, - rootValue _: Any, - variableValues _: [String: Map], - operation _: OperationDefinition?, - errors _: [GraphQLError], - result _: Map - ) { -// operationExecutionCalled += 1 -// XCTAssertEqual(processId, expectedProcessId, "unexpected process id") -// XCTAssertEqual(threadId, expectedThreadId, "unexpected thread id") -// XCTAssertGreaterThan(finished, started) -// XCTAssertTrue(schema === self.schema) -// XCTAssertEqual(document.loc?.source.name ?? "", "GraphQL request") -// XCTAssertTrue(rootValue as! MyRoot === expectedRoot) -// XCTAssertTrue(contextValue as! MyCtx === expectedCtx) -// XCTAssertEqual(variableValues, expectedOpVars) -// XCTAssertEqual(operation!.name!.value, expectedOpName) -// XCTAssertEqual(errors, []) -// XCTAssertEqual(result, expectedResult) - } - - func fieldResolution( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - source _: Any, - args _: Map, - info _: GraphQLResolveInfo, - result _: Result - ) { - fieldResolutionCalled += 1 -// XCTAssertEqual(processId, expectedProcessId, "unexpected process id") -// XCTAssertEqual(threadId, expectedThreadId, "unexpected thread id") -// XCTAssertGreaterThan(finished, started) -// XCTAssertTrue(source as! MyRoot === expectedRoot) -// XCTAssertEqual(args, try! expectedOpVars.asMap()) -// XCTAssertTrue(context as! MyCtx === expectedCtx) -// switch result { -// case .error(let e): -// XCTFail("unexpected error \(e)") -// case .result(let r): -// XCTAssertEqual(r as! String, try! expectedResult["data"]["hello"].asString()) -// } - } - - func testInstrumentationCalls() throws { -// #if os(Linux) || os(Android) -// expectedThreadId = Int(pthread_self()) -// #else -// expectedThreadId = Int(pthread_mach_thread_np(pthread_self())) -// #endif -// expectedProcessId = Int(getpid()) -// let result = try graphql( -// instrumentation: self, -// schema: schema, -// request: query, -// rootValue: expectedRoot, -// contextValue: expectedCtx, -// variableValues: expectedOpVars, -// operationName: expectedOpName -// ) -// XCTAssertEqual(result, expectedResult) -// XCTAssertEqual(queryParsingCalled, 1) -// XCTAssertEqual(queryValidationCalled, 1) -// XCTAssertEqual(operationExecutionCalled, 1) -// XCTAssertEqual(fieldResolutionCalled, 1) - } - - func testDispatchQueueInstrumentationWrapper() throws { -// let dispatchGroup = DispatchGroup() -// #if os(Linux) || os(Android) -// expectedThreadId = Int(pthread_self()) -// #else -// expectedThreadId = Int(pthread_mach_thread_np(pthread_self())) -// #endif -// expectedProcessId = Int(getpid()) -// let result = try graphql( -// instrumentation: DispatchQueueInstrumentationWrapper(self, dispatchGroup: dispatchGroup), -// schema: schema, -// request: query, -// rootValue: expectedRoot, -// contextValue: expectedCtx, -// variableValues: expectedOpVars, -// operationName: expectedOpName -// ) -// dispatchGroup.wait() -// XCTAssertEqual(result, expectedResult) -// XCTAssertEqual(queryParsingCalled, 1) -// XCTAssertEqual(queryValidationCalled, 1) -// XCTAssertEqual(operationExecutionCalled, 1) -// XCTAssertEqual(fieldResolutionCalled, 1) - } -} diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index 53d4bab1..9a49c80c 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -190,7 +190,6 @@ func createSubscription( queryStrategy: SerialFieldExecutionStrategy(), mutationStrategy: SerialFieldExecutionStrategy(), subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, schema: schema, request: query, rootValue: (), From 1bc642c06f9c7447e2f5c31ed700c7af901ca8c2 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 26 Jun 2025 22:44:25 -0600 Subject: [PATCH 5/8] test: Fixes race condition in test This resolves the race condition caused by the inbox counts and the event delivery. If event delivery happens before the subsequent publish increments the inbox counts, then the counts will be lower than expected. Resolved by just not asking for inbox counts, since they aren't relevant to the test. --- .../SubscriptionTests/SubscriptionTests.swift | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index 8711991b..e34afe49 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -711,10 +711,6 @@ class SubscriptionTests: XCTestCase { from subject } - inbox { - unread - total - } } } """) @@ -734,6 +730,8 @@ class SubscriptionTests: XCTestCase { )) let result1 = try await iterator.next() + let result2 = try await iterator.next() + XCTAssertEqual( result1, GraphQLResult( @@ -742,15 +740,9 @@ class SubscriptionTests: XCTestCase { "from": "yuzhi@graphql.org", "subject": "Alright", ], - "inbox": [ - "unread": 2, - "total": 3, - ], ]] ) ) - - let result2 = try await iterator.next() XCTAssertEqual( result2, GraphQLResult( @@ -759,10 +751,6 @@ class SubscriptionTests: XCTestCase { "from": "yuzhi@graphql.org", "subject": "Message 2", ], - "inbox": [ - "unread": 2, - "total": 3, - ], ]] ) ) From d25254b60a31437b1be017e3e322480f71d22321 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 1 Jul 2025 17:10:56 -0600 Subject: [PATCH 6/8] test: Moves test off of global dispatch queue This was causing test hangs on macOS --- .../FieldExecutionStrategyTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift index cf8e09e1..3caee2e0 100644 --- a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift +++ b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift @@ -2,6 +2,8 @@ import Dispatch @testable import GraphQL import XCTest +let queue = DispatchQueue(label: "testQueue") + class FieldExecutionStrategyTests: XCTestCase { enum StrategyError: Error { case exampleError(msg: String) @@ -24,7 +26,7 @@ class FieldExecutionStrategyTests: XCTestCase { let group = DispatchGroup() group.enter() - DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) { + queue.asyncAfter(wallDeadline: .now() + 0.1) { group.leave() } @@ -41,7 +43,7 @@ class FieldExecutionStrategyTests: XCTestCase { let g = DispatchGroup() g.enter() - DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) { + queue.asyncAfter(wallDeadline: .now() + 0.1) { g.leave() } From 631650a9ea3cb6a9f749e3192a7e06ba8ff00586 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 4 Jul 2025 23:39:59 -0600 Subject: [PATCH 7/8] feat!: Switches SubscriptionResult with Result --- MIGRATION.md | 4 + Sources/GraphQL/GraphQL.swift | 15 +-- Sources/GraphQL/Subscription/Subscribe.swift | 103 +++++++----------- .../SubscriptionSchema.swift | 7 +- .../SubscriptionTests/SubscriptionTests.swift | 31 +++--- 5 files changed, 68 insertions(+), 92 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 325fc0cd..73fa8534 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -20,6 +20,10 @@ This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. The `EventStream` abstraction used to provide pre-concurrency subscription support has been removed. This means that `graphqlSubscribe(...).stream` will now be an `AsyncThrowingStream` type, instead of an `EventStream` type, and that downcasting to `ConcurrentEventStream` is no longer necessary. +### SubscriptionResult removal + +The `SubscriptionResult` type was removed, and `graphqlSubscribe` now returns a true Swift `Result` type. + ### Instrumentation removal The `Instrumentation` type has been removed, with anticipated support for tracing using [`swift-distributed-tracing`](https://github.com/apple/swift-distributed-tracing). `instrumentation` arguments must be removed from `graphql` and `graphqlSubscribe` calls. diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 36bc3456..525b1970 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -42,16 +42,11 @@ public struct GraphQLResult: Equatable, Codable, Sendable, CustomStringConvertib } } -/// SubscriptionResult wraps the observable and error data returned by the subscribe request. -public struct SubscriptionResult { - public let stream: AsyncThrowingStream? +/// A collection of GraphQL errors. Enables returning multiple errors from Result types. +public struct GraphQLErrors: Error, Sendable { public let errors: [GraphQLError] - public init( - stream: AsyncThrowingStream? = nil, - errors: [GraphQLError] = [] - ) { - self.stream = stream + public init(_ errors: [GraphQLError]) { self.errors = errors } } @@ -228,7 +223,7 @@ public func graphqlSubscribe( context: Any = (), variableValues: [String: Map] = [:], operationName: String? = nil -) async throws -> SubscriptionResult { +) async throws -> Result, GraphQLErrors> { let source = Source(body: request, name: "GraphQL Subscription request") let documentAST = try parse(source: source) let validationErrors = validate( @@ -238,7 +233,7 @@ public func graphqlSubscribe( ) guard validationErrors.isEmpty else { - return SubscriptionResult(errors: validationErrors) + return .failure(.init(validationErrors)) } return try await subscribe( diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 82f7b842..dd1c6d5f 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -3,22 +3,16 @@ import OrderedCollections /** * Implements the "Subscribe" algorithm described in the GraphQL specification. * - * Returns a future which resolves to a SubscriptionResult containing either - * a SubscriptionObservable (if successful), or GraphQLErrors (error). + * Returns a `Result` that either succeeds with an `AsyncThrowingStream`, or fails with `GraphQLErrors`. * * If the client-provided arguments to this function do not result in a - * compliant subscription, the future will resolve to a - * SubscriptionResult containing `errors` and no `observable`. + * compliant subscription, the `Result` will fails with descriptive errors. * * If the source stream could not be created due to faulty subscription - * resolver logic or underlying systems, the future will resolve to a - * SubscriptionResult containing `errors` and no `observable`. + * resolver logic or underlying systems, the `Result` will fail with errors. * - * If the operation succeeded, the future will resolve to a SubscriptionResult, - * containing an `observable` which yields a stream of GraphQLResults + * If the operation succeeded, the `Result` will succeed with an `AsyncThrowingStream` of `GraphQLResult`s * representing the response stream. - * - * Accepts either an object with named arguments, or individual arguments. */ func subscribe( queryStrategy: QueryFieldExecutionStrategy, @@ -30,7 +24,7 @@ func subscribe( context: Any, variableValues: [String: Map] = [:], operationName: String? = nil -) async throws -> SubscriptionResult { +) async throws -> Result, GraphQLErrors> { let sourceResult = try await createSourceEventStream( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, @@ -43,7 +37,7 @@ func subscribe( operationName: operationName ) - if let sourceStream = sourceResult.stream { + return sourceResult.map { sourceStream in // We must create a new AsyncSequence because AsyncSequence.map requires a concrete type // (which we cannot know), // and we need the result to be a concrete type. @@ -80,9 +74,7 @@ func subscribe( task.cancel() } } - return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) - } else { - return SubscriptionResult(errors: sourceResult.errors) + return subscriptionStream } } @@ -90,20 +82,16 @@ func subscribe( * Implements the "CreateSourceEventStream" algorithm described in the * GraphQL specification, resolving the subscription source event stream. * - * Returns a Future which resolves to a SourceEventStreamResult, containing - * either an Observable (if successful) or GraphQLErrors (error). + * Returns a Result that either succeeds with an `AsyncSequence` or fails with `GraphQLErrors`. * * If the client-provided arguments to this function do not result in a - * compliant subscription, the future will resolve to a - * SourceEventStreamResult containing `errors` and no `observable`. + * compliant subscription, the `Result` will fail with descriptive errors. * * 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`. + * resolver logic or underlying systems, the `Result` will fail with errors. * - * 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. + * If the operation succeeded, the `Result` will succeed with an AsyncSequence 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. @@ -123,32 +111,37 @@ func createSourceEventStream( context: Any, variableValues: [String: Map] = [:], operationName: String? = nil -) async throws -> SourceEventStreamResult { +) async throws -> Result { + // If a valid context cannot be created due to incorrect arguments, + // this will throw an error. + let exeContext = try buildExecutionContext( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + schema: schema, + documentAST: documentAST, + rootValue: rootValue, + context: context, + rawVariableValues: variableValues, + operationName: operationName + ) do { - // If a valid context cannot be created due to incorrect arguments, - // this will throw an error. - let exeContext = try buildExecutionContext( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - schema: schema, - documentAST: documentAST, - rootValue: rootValue, - context: context, - rawVariableValues: variableValues, - operationName: operationName - ) return try await executeSubscription(context: exeContext) } catch let error as GraphQLError { - return SourceEventStreamResult(errors: [error]) + // If it is a GraphQLError, report it as a failure. + return .failure(.init([error])) + } catch let errors as GraphQLErrors { + // If it is a GraphQLErrors, report it as a failure. + return .failure(errors) } catch { - return SourceEventStreamResult(errors: [GraphQLError(error)]) + // Otherwise treat the error as a system-class error and re-throw it. + throw error } } func executeSubscription( context: ExecutionContext -) async throws -> SourceEventStreamResult { +) async throws -> Result { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) var inputFields: OrderedDictionary = [:] @@ -238,35 +231,21 @@ func executeSubscription( resolved = success } if !context.errors.isEmpty { - return SourceEventStreamResult(errors: context.errors) + return .failure(.init(context.errors)) } else if let error = resolved as? GraphQLError { - return SourceEventStreamResult(errors: [error]) + return .failure(.init([error])) } else if let stream = resolved as? any AsyncSequence { - return SourceEventStreamResult(stream: stream) + return .success(stream) } else if resolved == nil { - return SourceEventStreamResult(errors: [ + return .failure(.init([ GraphQLError(message: "Resolved subscription was nil"), - ]) + ])) } else { let resolvedObj = resolved as AnyObject - return SourceEventStreamResult(errors: [ + return .failure(.init([ GraphQLError( message: "Subscription field resolver must return an AsyncSequence. 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 stream: (any AsyncSequence)? - public let errors: [GraphQLError] - - public init(stream: (any AsyncSequence)? = nil, errors: [GraphQLError] = []) { - self.stream = stream - self.errors = errors + ])) } } diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index 9a49c80c..1670347d 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -197,10 +197,5 @@ func createSubscription( variableValues: variableValues, operationName: nil ) - - if let stream = result.stream { - return stream - } else { - throw result.errors.first! // We may have more than one... - } + return try result.get() } diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index e34afe49..660737ff 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -30,10 +30,7 @@ class SubscriptionTests: XCTestCase { schema: schema, request: query ) - guard let stream = subscriptionResult.stream else { - XCTFail(subscriptionResult.errors.description) - return - } + let stream = try subscriptionResult.get() var iterator = stream.makeAsyncIterator() db.trigger(email: Email( @@ -241,15 +238,19 @@ class SubscriptionTests: XCTestCase { """) XCTFail("Error should have been thrown") } catch { - guard let graphQLError = error as? GraphQLError else { - XCTFail("Error was not of type GraphQLError") + guard let graphQLErrors = error as? GraphQLErrors else { + XCTFail("Error was not of type GraphQLErrors") return } XCTAssertEqual( - graphQLError.message, - "Cannot query field \"unknownField\" on type \"Subscription\"." + graphQLErrors.errors, + [ + GraphQLError( + message: "Cannot query field \"unknownField\" on type \"Subscription\".", + locations: [SourceLocation(line: 2, column: 5)] + ), + ] ) - XCTAssertEqual(graphQLError.locations, [SourceLocation(line: 2, column: 5)]) } } @@ -284,13 +285,15 @@ class SubscriptionTests: XCTestCase { """) XCTFail("Error should have been thrown") } catch { - guard let graphQLError = error as? GraphQLError else { + guard let graphQLErrors = error as? GraphQLErrors else { XCTFail("Error was not of type GraphQLError") return } XCTAssertEqual( - graphQLError.message, - "Subscription field resolver must return an AsyncSequence. Received: 'test'" + graphQLErrors.errors, + [GraphQLError( + message: "Subscription field resolver must return an AsyncSequence. Received: 'test'" + )] ) } } @@ -310,11 +313,11 @@ class SubscriptionTests: XCTestCase { """) XCTFail("Error should have been thrown") } catch { - guard let graphQLError = error as? GraphQLError else { + guard let graphQLErrors = error as? GraphQLErrors else { XCTFail("Error was not of type GraphQLError") return } - XCTAssertEqual(graphQLError.message, "test error") + XCTAssertEqual(graphQLErrors.errors, [GraphQLError(message: "test error")]) } } From 34268d3ccd53f92d77ac54583a34172e001ae7a8 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sat, 5 Jul 2025 00:15:08 -0600 Subject: [PATCH 8/8] docs: Updates subscription docs in README --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8f94cb5b..284de788 100644 --- a/README.md +++ b/README.md @@ -57,12 +57,9 @@ The result of this query is a `GraphQLResult` that encodes to the following JSON ### Subscription -This package supports GraphQL subscription, but until the integration of `AsyncSequence` in Swift 5.5 the standard Swift library did not -provide an event-stream construct. For historical reasons and backwards compatibility, this library implements subscriptions using an -`EventStream` protocol that nearly every asynchronous stream implementation can conform to. - -To create a subscription field in a GraphQL schema, use the `subscribe` resolver that returns an `EventStream`. You must also provide a -`resolver`, which defines how to process each event as it occurs and must return the field result type. Here is an example: +This package supports GraphQL subscription. To create a subscription field in a GraphQL schema, use the `subscribe` +resolver that returns any type that conforms to `AsyncSequence`. You must also provide a `resolver`, which defines how +to process each event as it occurs and must return the field result type. Here is an example: ```swift let schema = try GraphQLSchema( @@ -71,16 +68,16 @@ let schema = try GraphQLSchema( fields: [ "hello": GraphQLField( type: GraphQLString, - resolve: { eventResult, _, _, _, _ in // Defines how to transform each event when it occurs + resolve: { eventResult, _, _, _ in // Defines how to transform each event when it occurs return eventResult }, - subscribe: { _, _, _, _, _ in // Defines how to construct the event stream + subscribe: { _, _, _, _ in // Defines how to construct the event stream return AsyncThrowingStream { continuation in let timer = Timer.scheduledTimer( withTimeInterval: 3, repeats: true, ) { - continuation.yield("world") // Emits "world" every 3 seconds + continuation.yield("world") // Emits "world" every 3 seconds } } } @@ -96,7 +93,8 @@ To execute a subscription use the `graphqlSubscribe` function: let subscriptionResult = try await graphqlSubscribe( schema: schema, ) -for try await result in concurrentStream.stream { +let stream = subscriptionResult.get() +for try await result in stream { print(result) } ```