Skip to content

Commit 135b1d7

Browse files
gh-action-runnergh-action-runner
gh-action-runner
authored and
gh-action-runner
committed
Squashed 'apollo-ios/' changes from 06c1a891..b73d4bb8
b73d4bb8 feature: GraphQL execution for `@defer` support (#413) git-subtree-dir: apollo-ios git-subtree-split: b73d4bb836ec44ea3536d506b2a354eed9986ea7
1 parent fee4ef7 commit 135b1d7

33 files changed

+1474
-356
lines changed

Design/3093-graphql-defer.md

+3-30
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public struct Fragments: FragmentContainer {
6666
@Deferred public var deferredFragmentFoo: DeferredFragmentFoo?
6767
}
6868

69-
public struct DeferredFragmentFoo: AnimalKingdomAPI.InlineFragment, ApolloAPI.Deferrable {
69+
public struct DeferredFragmentFoo: AnimalKingdomAPI.InlineFragment {
7070
}
7171
```
7272

@@ -147,36 +147,9 @@ In the preview release of `@defer`, operations with deferred fragments will **no
147147

148148
### Request header
149149

150-
If an operation can support an incremental delivery response it must add an `Accept` header to the HTTP request specifying the protocol version that can be parsed. An [example](https://github.com/apollographql/apollo-ios/blob/spike/defer/Sources/Apollo/RequestChainNetworkTransport.swift#L115) is HTTP subscription requests that include the `subscriptionSpec=1.0` specification. `@defer` would introduce another operation feature that would request an incremental delivery response.
150+
If an operation can support an incremental delivery response it must add an `Accept` header to the HTTP request specifying the protocol version that can be parsed in the response. An [example](https://github.com/apollographql/apollo-ios/blob/spike/defer/Sources/Apollo/RequestChainNetworkTransport.swift#L115) is HTTP subscription requests that include the `subscriptionSpec=1.0` specification. `@defer` introduces another incremental delivery response protocol. The defer response specification supported at the time of development is `deferSpec=20220824`.
151151

152-
This should not be sent with all requests though so operations will need to be identifiable as having deferred fragments to signal inclusion of the request header.
153-
154-
```swift
155-
// Sample code for RequestChainNetworkTransport
156-
open func constructRequest<Operation: GraphQLOperation>(
157-
for operation: Operation,
158-
cachePolicy: CachePolicy,
159-
contextIdentifier: UUID? = nil
160-
) -> HTTPRequest<Operation> {
161-
let request = ... // build request
162-
163-
if Operation.hasDeferredFragments {
164-
request.addHeader(
165-
name: "Accept",
166-
value: "multipart/mixed;boundary=\"graphql\";deferSpec=20220824,application/json"
167-
)
168-
}
169-
170-
return request
171-
}
172-
173-
// Sample of new property on GraphQLOperation
174-
public protocol GraphQLOperation: AnyObject, Hashable {
175-
// other properties not shown
176-
177-
static var hasDeferredFragments: Bool { get } // computed for each operation during codegen
178-
}
179-
```
152+
All operations will have an `Accept` header specifying the supported incremental delivery response protocol; Subscription operations will have the `subscriptionSpec` protocol, Query and Mutation operations will have the `deferSpec` protocol in the `Accept` header.
180153

181154
### Response parsing
182155

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#if !COCOAPODS
2+
import ApolloAPI
3+
#endif
4+
5+
/// An abstract GraphQL response used for full and incremental responses.
6+
struct AnyGraphQLResponse {
7+
let body: JSONObject
8+
9+
private let rootKey: CacheReference
10+
private let variables: GraphQLOperation.Variables?
11+
12+
init(
13+
body: JSONObject,
14+
rootKey: CacheReference,
15+
variables: GraphQLOperation.Variables?
16+
) {
17+
self.body = body
18+
self.rootKey = rootKey
19+
self.variables = variables
20+
}
21+
22+
/// Call this function when you want to execute on an entire operation and its response data.
23+
/// This function should also be called to execute on the partial (initial) response of an
24+
/// operation with deferred selection sets.
25+
func execute<
26+
Accumulator: GraphQLResultAccumulator,
27+
Data: RootSelectionSet
28+
>(
29+
selectionSet: Data.Type,
30+
with accumulator: Accumulator
31+
) throws -> Accumulator.FinalResult? {
32+
guard let dataEntry = body["data"] as? JSONObject else {
33+
return nil
34+
}
35+
36+
return try executor.execute(
37+
selectionSet: Data.self,
38+
on: dataEntry,
39+
withRootCacheReference: rootKey,
40+
variables: variables,
41+
accumulator: accumulator
42+
)
43+
}
44+
45+
/// Call this function to execute on a specific selection set and its incremental response data.
46+
/// This is typically used when executing on deferred selections.
47+
func execute<
48+
Accumulator: GraphQLResultAccumulator,
49+
Operation: GraphQLOperation
50+
>(
51+
selectionSet: any Deferrable.Type,
52+
in operation: Operation.Type,
53+
with accumulator: Accumulator
54+
) throws -> Accumulator.FinalResult? {
55+
guard let dataEntry = body["data"] as? JSONObject else {
56+
return nil
57+
}
58+
59+
return try executor.execute(
60+
selectionSet: selectionSet,
61+
in: Operation.self,
62+
on: dataEntry,
63+
withRootCacheReference: rootKey,
64+
variables: variables,
65+
accumulator: accumulator
66+
)
67+
}
68+
69+
var executor: GraphQLExecutor<NetworkResponseExecutionSource> {
70+
GraphQLExecutor(executionSource: NetworkResponseExecutionSource())
71+
}
72+
73+
func parseErrors() -> [GraphQLError]? {
74+
guard let errorsEntry = self.body["errors"] as? [JSONObject] else {
75+
return nil
76+
}
77+
78+
return errorsEntry.map(GraphQLError.init)
79+
}
80+
81+
func parseExtensions() -> JSONObject? {
82+
return self.body["extensions"] as? JSONObject
83+
}
84+
}
85+
86+
// MARK: - Equatable Conformance
87+
88+
extension AnyGraphQLResponse: Equatable {
89+
static func == (lhs: AnyGraphQLResponse, rhs: AnyGraphQLResponse) -> Bool {
90+
lhs.body == rhs.body &&
91+
lhs.rootKey == rhs.rootKey &&
92+
lhs.variables?._jsonEncodableObject._jsonValue == rhs.variables?._jsonEncodableObject._jsonValue
93+
}
94+
}
95+
96+
// MARK: - Hashable Conformance
97+
98+
extension AnyGraphQLResponse: Hashable {
99+
func hash(into hasher: inout Hasher) {
100+
hasher.combine(body)
101+
hasher.combine(rootKey)
102+
hasher.combine(variables?._jsonEncodableObject._jsonValue)
103+
}
104+
}

Sources/Apollo/CacheWriteInterceptor.swift

+23-31
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ import ApolloAPI
55

66
/// An interceptor which writes data to the cache, following the `HTTPRequest`'s `cachePolicy`.
77
public struct CacheWriteInterceptor: ApolloInterceptor {
8-
8+
99
public enum CacheWriteError: Error, LocalizedError {
10+
@available(*, deprecated, message: "Will be removed in a future version.")
1011
case noResponseToParse
11-
12+
13+
case missingCacheRecords
14+
1215
public var errorDescription: String? {
1316
switch self {
1417
case .noResponseToParse:
1518
return "The Cache Write Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors."
19+
case .missingCacheRecords:
20+
return "The Cache Write Interceptor cannot find any cache records. Double-check the order of your interceptors."
1621
}
1722
}
1823
}
@@ -43,44 +48,31 @@ public struct CacheWriteInterceptor: ApolloInterceptor {
4348
)
4449
return
4550
}
46-
51+
4752
guard
4853
let createdResponse = response,
49-
let legacyResponse = createdResponse.legacyResponse else {
54+
let cacheRecords = createdResponse.cacheRecords
55+
else {
5056
chain.handleErrorAsync(
51-
CacheWriteError.noResponseToParse,
57+
CacheWriteError.missingCacheRecords,
5258
request: request,
5359
response: response,
5460
completion: completion
5561
)
56-
return
62+
return
5763
}
58-
59-
do {
60-
let (_, records) = try legacyResponse.parseResult()
61-
62-
guard !chain.isCancelled else {
63-
return
64-
}
65-
66-
if let records = records {
67-
self.store.publish(records: records, identifier: request.contextIdentifier)
68-
}
69-
70-
chain.proceedAsync(
71-
request: request,
72-
response: createdResponse,
73-
interceptor: self,
74-
completion: completion
75-
)
7664

77-
} catch {
78-
chain.handleErrorAsync(
79-
error,
80-
request: request,
81-
response: response,
82-
completion: completion
83-
)
65+
guard !chain.isCancelled else {
66+
return
8467
}
68+
69+
self.store.publish(records: cacheRecords, identifier: request.contextIdentifier)
70+
71+
chain.proceedAsync(
72+
request: request,
73+
response: createdResponse,
74+
interceptor: self,
75+
completion: completion
76+
)
8577
}
8678
}

0 commit comments

Comments
 (0)