diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 7794f1c7..d8f4d281 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -149,6 +149,7 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, * * Each field is resolved as an individual task on a concurrent dispatch queue. */ +@available(*, deprecated, message: "Use ConcurrentFieldExecutionStrategy instead") public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrategy, SubscriptionFieldExecutionStrategy { @@ -224,6 +225,37 @@ public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrat } } +/** + * Serial field execution strategy that's suitable for the "Evaluating selection sets" section of the spec for "read" mode. + */ +public struct ConcurrentFieldExecutionStrategy: QueryFieldExecutionStrategy, +SubscriptionFieldExecutionStrategy { + public init() {} + + public func executeFields( + exeContext: ExecutionContext, + parentType: GraphQLObjectType, + sourceValue: Any, + path: IndexPath, + fields: OrderedDictionary + ) throws -> Future> { + var results = OrderedDictionary?>(minimumCapacity: fields.count) + for field in fields { + let fieldASTs = field.value + let fieldKey = field.key + let fieldPath = path.appending(fieldKey) + results[fieldKey] = try resolveField( + exeContext: exeContext, + parentType: parentType, + source: sourceValue, + fieldASTs: fieldASTs, + path: fieldPath + ).map { $0 ?? Map.null } + } + return results.compactMapValues { $0 }.flatten(on: exeContext.eventLoopGroup) + } +} + /** * Implements the "Evaluating requests" section of the GraphQL specification. * diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 76138126..26fae750 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -66,11 +66,6 @@ public typealias SubscriptionEventStream = EventStream> /// 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 @@ -92,9 +87,40 @@ public typealias SubscriptionEventStream = EventStream> /// and there will be an error inside `errors` specifying the reason for the failure and the path of /// the failed field. public func graphql( - queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), + schema: GraphQLSchema, + request: String, + rootValue: Any = (), + context: Any = (), + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil, + validationRules: [(ValidationContext) -> Visitor] = [] +) throws -> Future { + return try graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: ConcurrentFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + validationRules: validationRules, + schema: schema, + request: request, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ) +} + +@available( + *, + deprecated, + message: "Specifying exeuction strategies and instrumentation will be removed in a future version." +) +public func graphql( + queryStrategy: QueryFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), - subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), + subscriptionStrategy: SubscriptionFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), instrumentation: Instrumentation = NoOpInstrumentation, validationRules: [(ValidationContext) -> Visitor] = [], schema: GraphQLSchema, @@ -161,9 +187,38 @@ public func graphql( /// and there will be an error inside `errors` specifying the reason for the failure and the path of /// the failed field. public func graphql( - queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), + queryRetrieval: Retrieval, + queryId: Retrieval.Id, + rootValue: Any = (), + context: Any = (), + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil +) throws -> Future { + return try graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: ConcurrentFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + queryRetrieval: queryRetrieval, + queryId: queryId, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ) +} + +@available( + *, + deprecated, + message: "Specifying exeuction strategies and instrumentation will be removed in a future version." +) +public func graphql( + queryStrategy: QueryFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), - subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), + subscriptionStrategy: SubscriptionFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), instrumentation: Instrumentation = NoOpInstrumentation, queryRetrieval: Retrieval, queryId: Retrieval.Id, @@ -235,9 +290,40 @@ public func graphql( /// will be an error inside `errors` specifying the reason for the failure and the path of the /// failed field. public func graphqlSubscribe( - queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), + schema: GraphQLSchema, + request: String, + rootValue: Any = (), + context: Any = (), + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil, + validationRules: [(ValidationContext) -> Visitor] = [] +) throws -> Future { + return try graphqlSubscribe( + queryStrategy: ConcurrentFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: ConcurrentFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + validationRules: validationRules, + schema: schema, + request: request, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ) +} + +@available( + *, + deprecated, + message: "Specifying exeuction strategies and instrumentation will be removed in a future version." +) +public func graphqlSubscribe( + queryStrategy: QueryFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), - subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), + subscriptionStrategy: SubscriptionFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), instrumentation: Instrumentation = NoOpInstrumentation, validationRules: [(ValidationContext) -> Visitor] = [], schema: GraphQLSchema, @@ -316,9 +402,35 @@ public func graphqlSubscribe( /// 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(), + schema: GraphQLSchema, + request: String, + rootValue: Any = (), + context: Any = (), + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil +) async throws -> GraphQLResult { + return try await graphql( + schema: schema, + request: request, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ).get() +} + +@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) +@available( + *, + deprecated, + message: "Specifying exeuction strategies and instrumentation will be removed in a future version." +) +public func graphql( + queryStrategy: QueryFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), - subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), + subscriptionStrategy: SubscriptionFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), instrumentation: Instrumentation = NoOpInstrumentation, schema: GraphQLSchema, request: String, @@ -383,9 +495,35 @@ public func graphql( /// failed field. @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) public func graphqlSubscribe( - queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), + schema: GraphQLSchema, + request: String, + rootValue: Any = (), + context: Any = (), + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil +) async throws -> SubscriptionResult { + return try await graphqlSubscribe( + schema: schema, + request: request, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ).get() +} + +@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) +@available( + *, + deprecated, + message: "Specifying exeuction strategies and instrumentation will be removed in a future version." +) +public func graphqlSubscribe( + queryStrategy: QueryFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), - subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), + subscriptionStrategy: SubscriptionFieldExecutionStrategy = ConcurrentFieldExecutionStrategy(), instrumentation: Instrumentation = NoOpInstrumentation, schema: GraphQLSchema, request: String, diff --git a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift index cd021f3b..8c1bf1c2 100644 --- a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift +++ b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift @@ -313,4 +313,49 @@ class FieldExecutionStrategyTests: XCTestCase { } // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } + + func testConcurrentFieldExecutionStrategyWithSingleField() throws { + let result = try timing(graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), + schema: schema, + request: singleQuery, + eventLoopGroup: eventLoopGroup + ).wait()) + XCTAssertEqual(result.value, singleExpected) + } + + func testConcurrentFieldExecutionStrategyWithSingleFieldError() throws { + let result = try timing(graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), + schema: schema, + request: singleThrowsQuery, + eventLoopGroup: eventLoopGroup + ).wait()) + XCTAssertEqual(result.value, singleThrowsExpected) + } + + func testConcurrentFieldExecutionStrategyWithMultipleFields() throws { + let result = try timing(graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), + schema: schema, + request: multiQuery, + eventLoopGroup: eventLoopGroup + ).wait()) + XCTAssertEqual(result.value, multiExpected) + } + + func testConcurrentFieldExecutionStrategyWithMultipleFieldErrors() throws { + let result = try timing(graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), + schema: schema, + request: multiThrowsQuery, + eventLoopGroup: eventLoopGroup + ).wait()) + XCTAssertEqual(result.value.data, multiThrowsExpectedData) + let resultErrors = result.value.errors + XCTAssertEqual(resultErrors.count, multiThrowsExpectedErrors.count) + for m in multiThrowsExpectedErrors { + XCTAssertTrue(resultErrors.contains(m), "Expecting result errors to contain \(m)") + } + } } diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index fb940c72..ec134860 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -193,10 +193,6 @@ func createSubscription( variableValues: [String: Map] = [:] ) throws -> SubscriptionEventStream { let result = try graphqlSubscribe( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, schema: schema, request: query, rootValue: (),