Skip to content

Commit 631650a

Browse files
feat!: Switches SubscriptionResult with Result
1 parent d25254b commit 631650a

File tree

5 files changed

+68
-92
lines changed

5 files changed

+68
-92
lines changed

MIGRATION.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters.
2020

2121
The `EventStream` abstraction used to provide pre-concurrency subscription support has been removed. This means that `graphqlSubscribe(...).stream` will now be an `AsyncThrowingStream<GraphQLResult, Error>` type, instead of an `EventStream` type, and that downcasting to `ConcurrentEventStream` is no longer necessary.
2222

23+
### SubscriptionResult removal
24+
25+
The `SubscriptionResult` type was removed, and `graphqlSubscribe` now returns a true Swift `Result` type.
26+
2327
### Instrumentation removal
2428

2529
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.

Sources/GraphQL/GraphQL.swift

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,11 @@ public struct GraphQLResult: Equatable, Codable, Sendable, CustomStringConvertib
4242
}
4343
}
4444

45-
/// SubscriptionResult wraps the observable and error data returned by the subscribe request.
46-
public struct SubscriptionResult {
47-
public let stream: AsyncThrowingStream<GraphQLResult, Error>?
45+
/// A collection of GraphQL errors. Enables returning multiple errors from Result types.
46+
public struct GraphQLErrors: Error, Sendable {
4847
public let errors: [GraphQLError]
4948

50-
public init(
51-
stream: AsyncThrowingStream<GraphQLResult, Error>? = nil,
52-
errors: [GraphQLError] = []
53-
) {
54-
self.stream = stream
49+
public init(_ errors: [GraphQLError]) {
5550
self.errors = errors
5651
}
5752
}
@@ -228,7 +223,7 @@ public func graphqlSubscribe(
228223
context: Any = (),
229224
variableValues: [String: Map] = [:],
230225
operationName: String? = nil
231-
) async throws -> SubscriptionResult {
226+
) async throws -> Result<AsyncThrowingStream<GraphQLResult, Error>, GraphQLErrors> {
232227
let source = Source(body: request, name: "GraphQL Subscription request")
233228
let documentAST = try parse(source: source)
234229
let validationErrors = validate(
@@ -238,7 +233,7 @@ public func graphqlSubscribe(
238233
)
239234

240235
guard validationErrors.isEmpty else {
241-
return SubscriptionResult(errors: validationErrors)
236+
return .failure(.init(validationErrors))
242237
}
243238

244239
return try await subscribe(

Sources/GraphQL/Subscription/Subscribe.swift

Lines changed: 41 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,16 @@ import OrderedCollections
33
/**
44
* Implements the "Subscribe" algorithm described in the GraphQL specification.
55
*
6-
* Returns a future which resolves to a SubscriptionResult containing either
7-
* a SubscriptionObservable (if successful), or GraphQLErrors (error).
6+
* Returns a `Result` that either succeeds with an `AsyncThrowingStream`, or fails with `GraphQLErrors`.
87
*
98
* If the client-provided arguments to this function do not result in a
10-
* compliant subscription, the future will resolve to a
11-
* SubscriptionResult containing `errors` and no `observable`.
9+
* compliant subscription, the `Result` will fails with descriptive errors.
1210
*
1311
* If the source stream could not be created due to faulty subscription
14-
* resolver logic or underlying systems, the future will resolve to a
15-
* SubscriptionResult containing `errors` and no `observable`.
12+
* resolver logic or underlying systems, the `Result` will fail with errors.
1613
*
17-
* If the operation succeeded, the future will resolve to a SubscriptionResult,
18-
* containing an `observable` which yields a stream of GraphQLResults
14+
* If the operation succeeded, the `Result` will succeed with an `AsyncThrowingStream` of `GraphQLResult`s
1915
* representing the response stream.
20-
*
21-
* Accepts either an object with named arguments, or individual arguments.
2216
*/
2317
func subscribe(
2418
queryStrategy: QueryFieldExecutionStrategy,
@@ -30,7 +24,7 @@ func subscribe(
3024
context: Any,
3125
variableValues: [String: Map] = [:],
3226
operationName: String? = nil
33-
) async throws -> SubscriptionResult {
27+
) async throws -> Result<AsyncThrowingStream<GraphQLResult, Error>, GraphQLErrors> {
3428
let sourceResult = try await createSourceEventStream(
3529
queryStrategy: queryStrategy,
3630
mutationStrategy: mutationStrategy,
@@ -43,7 +37,7 @@ func subscribe(
4337
operationName: operationName
4438
)
4539

46-
if let sourceStream = sourceResult.stream {
40+
return sourceResult.map { sourceStream in
4741
// We must create a new AsyncSequence because AsyncSequence.map requires a concrete type
4842
// (which we cannot know),
4943
// and we need the result to be a concrete type.
@@ -80,30 +74,24 @@ func subscribe(
8074
task.cancel()
8175
}
8276
}
83-
return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors)
84-
} else {
85-
return SubscriptionResult(errors: sourceResult.errors)
77+
return subscriptionStream
8678
}
8779
}
8880

8981
/**
9082
* Implements the "CreateSourceEventStream" algorithm described in the
9183
* GraphQL specification, resolving the subscription source event stream.
9284
*
93-
* Returns a Future which resolves to a SourceEventStreamResult, containing
94-
* either an Observable (if successful) or GraphQLErrors (error).
85+
* Returns a Result that either succeeds with an `AsyncSequence` or fails with `GraphQLErrors`.
9586
*
9687
* If the client-provided arguments to this function do not result in a
97-
* compliant subscription, the future will resolve to a
98-
* SourceEventStreamResult containing `errors` and no `observable`.
88+
* compliant subscription, the `Result` will fail with descriptive errors.
9989
*
10090
* If the source stream could not be created due to faulty subscription
101-
* resolver logic or underlying systems, the future will resolve to a
102-
* SourceEventStreamResult containing `errors` and no `observable`.
91+
* resolver logic or underlying systems, the `Result` will fail with errors.
10392
*
104-
* If the operation succeeded, the future will resolve to a SubscriptionResult,
105-
* containing an `observable` which yields a stream of event objects
106-
* returned by the subscription resolver.
93+
* If the operation succeeded, the `Result` will succeed with an AsyncSequence for the
94+
* event stream returned by the resolver.
10795
*
10896
* A Source Event Stream represents a sequence of events, each of which triggers
10997
* a GraphQL execution for that event.
@@ -123,32 +111,37 @@ func createSourceEventStream(
123111
context: Any,
124112
variableValues: [String: Map] = [:],
125113
operationName: String? = nil
126-
) async throws -> SourceEventStreamResult {
114+
) async throws -> Result<any AsyncSequence, GraphQLErrors> {
115+
// If a valid context cannot be created due to incorrect arguments,
116+
// this will throw an error.
117+
let exeContext = try buildExecutionContext(
118+
queryStrategy: queryStrategy,
119+
mutationStrategy: mutationStrategy,
120+
subscriptionStrategy: subscriptionStrategy,
121+
schema: schema,
122+
documentAST: documentAST,
123+
rootValue: rootValue,
124+
context: context,
125+
rawVariableValues: variableValues,
126+
operationName: operationName
127+
)
127128
do {
128-
// If a valid context cannot be created due to incorrect arguments,
129-
// this will throw an error.
130-
let exeContext = try buildExecutionContext(
131-
queryStrategy: queryStrategy,
132-
mutationStrategy: mutationStrategy,
133-
subscriptionStrategy: subscriptionStrategy,
134-
schema: schema,
135-
documentAST: documentAST,
136-
rootValue: rootValue,
137-
context: context,
138-
rawVariableValues: variableValues,
139-
operationName: operationName
140-
)
141129
return try await executeSubscription(context: exeContext)
142130
} catch let error as GraphQLError {
143-
return SourceEventStreamResult(errors: [error])
131+
// If it is a GraphQLError, report it as a failure.
132+
return .failure(.init([error]))
133+
} catch let errors as GraphQLErrors {
134+
// If it is a GraphQLErrors, report it as a failure.
135+
return .failure(errors)
144136
} catch {
145-
return SourceEventStreamResult(errors: [GraphQLError(error)])
137+
// Otherwise treat the error as a system-class error and re-throw it.
138+
throw error
146139
}
147140
}
148141

149142
func executeSubscription(
150143
context: ExecutionContext
151-
) async throws -> SourceEventStreamResult {
144+
) async throws -> Result<any AsyncSequence, GraphQLErrors> {
152145
// Get the first node
153146
let type = try getOperationRootType(schema: context.schema, operation: context.operation)
154147
var inputFields: OrderedDictionary<String, [Field]> = [:]
@@ -238,35 +231,21 @@ func executeSubscription(
238231
resolved = success
239232
}
240233
if !context.errors.isEmpty {
241-
return SourceEventStreamResult(errors: context.errors)
234+
return .failure(.init(context.errors))
242235
} else if let error = resolved as? GraphQLError {
243-
return SourceEventStreamResult(errors: [error])
236+
return .failure(.init([error]))
244237
} else if let stream = resolved as? any AsyncSequence {
245-
return SourceEventStreamResult(stream: stream)
238+
return .success(stream)
246239
} else if resolved == nil {
247-
return SourceEventStreamResult(errors: [
240+
return .failure(.init([
248241
GraphQLError(message: "Resolved subscription was nil"),
249-
])
242+
]))
250243
} else {
251244
let resolvedObj = resolved as AnyObject
252-
return SourceEventStreamResult(errors: [
245+
return .failure(.init([
253246
GraphQLError(
254247
message: "Subscription field resolver must return an AsyncSequence. Received: '\(resolvedObj)'"
255248
),
256-
])
257-
}
258-
}
259-
260-
// Subscription resolvers MUST return observables that are declared as 'Any' due to Swift not having
261-
// covariant generic support for type
262-
// checking. Normal resolvers for subscription fields should handle type casting, same as resolvers
263-
// for query fields.
264-
struct SourceEventStreamResult {
265-
public let stream: (any AsyncSequence)?
266-
public let errors: [GraphQLError]
267-
268-
public init(stream: (any AsyncSequence)? = nil, errors: [GraphQLError] = []) {
269-
self.stream = stream
270-
self.errors = errors
249+
]))
271250
}
272251
}

Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,5 @@ func createSubscription(
197197
variableValues: variableValues,
198198
operationName: nil
199199
)
200-
201-
if let stream = result.stream {
202-
return stream
203-
} else {
204-
throw result.errors.first! // We may have more than one...
205-
}
200+
return try result.get()
206201
}

Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,7 @@ class SubscriptionTests: XCTestCase {
3030
schema: schema,
3131
request: query
3232
)
33-
guard let stream = subscriptionResult.stream else {
34-
XCTFail(subscriptionResult.errors.description)
35-
return
36-
}
33+
let stream = try subscriptionResult.get()
3734
var iterator = stream.makeAsyncIterator()
3835

3936
db.trigger(email: Email(
@@ -241,15 +238,19 @@ class SubscriptionTests: XCTestCase {
241238
""")
242239
XCTFail("Error should have been thrown")
243240
} catch {
244-
guard let graphQLError = error as? GraphQLError else {
245-
XCTFail("Error was not of type GraphQLError")
241+
guard let graphQLErrors = error as? GraphQLErrors else {
242+
XCTFail("Error was not of type GraphQLErrors")
246243
return
247244
}
248245
XCTAssertEqual(
249-
graphQLError.message,
250-
"Cannot query field \"unknownField\" on type \"Subscription\"."
246+
graphQLErrors.errors,
247+
[
248+
GraphQLError(
249+
message: "Cannot query field \"unknownField\" on type \"Subscription\".",
250+
locations: [SourceLocation(line: 2, column: 5)]
251+
),
252+
]
251253
)
252-
XCTAssertEqual(graphQLError.locations, [SourceLocation(line: 2, column: 5)])
253254
}
254255
}
255256

@@ -284,13 +285,15 @@ class SubscriptionTests: XCTestCase {
284285
""")
285286
XCTFail("Error should have been thrown")
286287
} catch {
287-
guard let graphQLError = error as? GraphQLError else {
288+
guard let graphQLErrors = error as? GraphQLErrors else {
288289
XCTFail("Error was not of type GraphQLError")
289290
return
290291
}
291292
XCTAssertEqual(
292-
graphQLError.message,
293-
"Subscription field resolver must return an AsyncSequence. Received: 'test'"
293+
graphQLErrors.errors,
294+
[GraphQLError(
295+
message: "Subscription field resolver must return an AsyncSequence. Received: 'test'"
296+
)]
294297
)
295298
}
296299
}
@@ -310,11 +313,11 @@ class SubscriptionTests: XCTestCase {
310313
""")
311314
XCTFail("Error should have been thrown")
312315
} catch {
313-
guard let graphQLError = error as? GraphQLError else {
316+
guard let graphQLErrors = error as? GraphQLErrors else {
314317
XCTFail("Error was not of type GraphQLError")
315318
return
316319
}
317-
XCTAssertEqual(graphQLError.message, "test error")
320+
XCTAssertEqual(graphQLErrors.errors, [GraphQLError(message: "test error")])
318321
}
319322
}
320323

0 commit comments

Comments
 (0)