Skip to content

Commit 0f10e94

Browse files
authored
fix: refactor GraphQL response decoder (#971)
* fix: refactor GraphQL response decoder * address PR comments
1 parent 492c0a3 commit 0f10e94

16 files changed

+717
-303
lines changed

AmplifyPlugins/API/APICategoryPlugin.xcodeproj/project.pbxproj

Lines changed: 57 additions & 17 deletions
Large diffs are not rendered by default.

AmplifyPlugins/API/AWSAPICategoryPlugin/AWSAPIPlugin+GraphQLBehavior.swift

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ public extension AWSAPIPlugin {
1111

1212
func query<R: Decodable>(request: GraphQLRequest<R>,
1313
listener: GraphQLOperation<R>.ResultListener?) -> GraphQLOperation<R> {
14-
let operationRequest = getOperationRequest(request: request,
15-
operationType: .query)
16-
17-
let operation = AWSGraphQLOperation(request: operationRequest,
14+
let operation = AWSGraphQLOperation(request: request.toOperationRequest(operationType: .query),
1815
session: session,
1916
mapper: mapper,
2017
pluginConfig: pluginConfig,
@@ -25,10 +22,7 @@ public extension AWSAPIPlugin {
2522

2623
func mutate<R: Decodable>(request: GraphQLRequest<R>,
2724
listener: GraphQLOperation<R>.ResultListener?) -> GraphQLOperation<R> {
28-
let operationRequest = getOperationRequest(request: request,
29-
operationType: .mutation)
30-
31-
let operation = AWSGraphQLOperation(request: operationRequest,
25+
let operation = AWSGraphQLOperation(request: request.toOperationRequest(operationType: .mutation),
3226
session: session,
3327
mapper: mapper,
3428
pluginConfig: pluginConfig,
@@ -42,11 +36,8 @@ public extension AWSAPIPlugin {
4236
valueListener: GraphQLSubscriptionOperation<R>.InProcessListener?,
4337
completionListener: GraphQLSubscriptionOperation<R>.ResultListener?
4438
) -> GraphQLSubscriptionOperation<R> {
45-
let operationRequest = getOperationRequest(request: request,
46-
operationType: .subscription)
47-
4839
let operation = AWSGraphQLSubscriptionOperation(
49-
request: operationRequest,
40+
request: request.toOperationRequest(operationType: .subscription),
5041
pluginConfig: pluginConfig,
5142
subscriptionConnectionFactory: subscriptionConnectionFactory,
5243
authService: authService,
@@ -56,17 +47,4 @@ public extension AWSAPIPlugin {
5647
queue.addOperation(operation)
5748
return operation
5849
}
59-
60-
private func getOperationRequest<R: Decodable>(request: GraphQLRequest<R>,
61-
operationType: GraphQLOperationType) -> GraphQLOperationRequest<R> {
62-
63-
let operationRequest = GraphQLOperationRequest(apiName: request.apiName,
64-
operationType: operationType,
65-
document: request.document,
66-
variables: request.variables,
67-
responseType: request.responseType,
68-
decodePath: request.decodePath,
69-
options: GraphQLOperationRequest.Options())
70-
return operationRequest
71-
}
7250
}

AmplifyPlugins/API/AWSAPICategoryPlugin/Operation/AWSGraphQLOperation+APIOperation.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ extension AWSGraphQLOperation: APIOperation {
3636
return
3737
}
3838

39-
graphQLResponseData.append(data)
39+
graphQLResponseDecoder.appendResponse(data)
4040
}
4141

4242
func complete(with error: Error?, response: URLResponse?) {
@@ -59,13 +59,7 @@ extension AWSGraphQLOperation: APIOperation {
5959
}
6060

6161
do {
62-
let graphQLServiceResponse = try GraphQLResponseDecoder.deserialize(graphQLResponse: graphQLResponseData)
63-
64-
let graphQLResponse = try GraphQLResponseDecoder.decode(graphQLServiceResponse: graphQLServiceResponse,
65-
responseType: request.responseType,
66-
decodePath: request.decodePath,
67-
rawGraphQLResponse: graphQLResponseData)
68-
62+
let graphQLResponse = try graphQLResponseDecoder.decodeToGraphQLResponse()
6963
dispatch(result: .success(graphQLResponse))
7064
finish()
7165
} catch let error as APIError {

AmplifyPlugins/API/AWSAPICategoryPlugin/Operation/AWSGraphQLOperation.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ final public class AWSGraphQLOperation<R: Decodable>: GraphQLOperation<R> {
1313
let session: URLSessionBehavior
1414
let mapper: OperationTaskMapper
1515
let pluginConfig: AWSAPICategoryPluginConfiguration
16-
17-
var graphQLResponseData = Data()
16+
let graphQLResponseDecoder: GraphQLResponseDecoder<R>
1817

1918
init(request: GraphQLOperationRequest<R>,
2019
session: URLSessionBehavior,
@@ -25,6 +24,7 @@ final public class AWSGraphQLOperation<R: Decodable>: GraphQLOperation<R> {
2524
self.session = session
2625
self.mapper = mapper
2726
self.pluginConfig = pluginConfig
27+
self.graphQLResponseDecoder = GraphQLResponseDecoder(request: request)
2828

2929
super.init(categoryType: .api,
3030
eventName: request.operationType.hubEventName,

AmplifyPlugins/API/AWSAPICategoryPlugin/Operation/AWSGraphQLSubscriptionOperation.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,8 @@ final public class AWSGraphQLSubscriptionOperation<R: Decodable>: GraphQLSubscri
147147

148148
private func onGraphQLResponseData(_ graphQLResponseData: Data) {
149149
do {
150-
let graphQLServiceResponse = try GraphQLResponseDecoder.deserialize(graphQLResponse: graphQLResponseData)
151-
let graphQLResponse = try GraphQLResponseDecoder.decode(graphQLServiceResponse: graphQLServiceResponse,
152-
responseType: request.responseType,
153-
decodePath: request.decodePath,
154-
rawGraphQLResponse: graphQLResponseData)
150+
let graphQLResponseDecoder = GraphQLResponseDecoder(request: request, response: graphQLResponseData)
151+
let graphQLResponse = try graphQLResponseDecoder.decodeToGraphQLResponse()
155152
dispatchInProcess(data: .data(graphQLResponse))
156153
} catch let error as APIError {
157154
dispatch(result: .failure(error))
@@ -169,7 +166,7 @@ final public class AWSGraphQLSubscriptionOperation<R: Decodable>: GraphQLSubscri
169166
let errorDescription = "Subscription item event failed with error"
170167
if case let ConnectionProviderError.subscription(_, payload) = error,
171168
let errors = payload?["errors"] as? AppSyncJSONValue,
172-
let graphQLErrors = try? GraphQLResponseDecoder.decodeAppSyncErrors(errors) {
169+
let graphQLErrors = try? GraphQLErrorDecoder.decodeAppSyncErrors(errors) {
173170
let graphQLResponseError = GraphQLResponseError<R>.error(graphQLErrors)
174171
dispatch(result: .failure(APIError.operationError(errorDescription, "", graphQLResponseError)))
175172
finish()
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
// SPDX-License-Identifier: Apache-2.0
66
//
77

8-
import Foundation
98
import Amplify
109
import AppSyncRealTimeClient
1110

12-
extension GraphQLResponseDecoder {
13-
11+
struct GraphQLErrorDecoder {
1412
static func decodeErrors(graphQLErrors: [JSONValue]) throws -> [GraphQLError] {
1513
var responseErrors = [GraphQLError]()
1614
for error in graphQLErrors {
@@ -35,7 +33,7 @@ extension GraphQLResponseDecoder {
3533
throw APIError.unknown("Expected 'errors' field not found in \(String(describing: appSyncJSON))", "", nil)
3634
}
3735
let convertedValues = errors.map(AppSyncJSONValue.toJSONValue)
38-
return try GraphQLResponseDecoder.decodeErrors(graphQLErrors: convertedValues)
36+
return try decodeErrors(graphQLErrors: convertedValues)
3937
}
4038

4139
static func decode(graphQLErrorJSON: JSONValue) throws -> GraphQLError {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Amplify
9+
import AWSPluginsCore
10+
11+
extension GraphQLResponseDecoder {
12+
13+
func decodeToResponseType(_ graphQLData: [String: JSONValue]) throws -> R {
14+
let graphQLData = try valueAtDecodePath(from: JSONValue.object(graphQLData))
15+
if request.responseType == String.self {
16+
let serializedJSON = try encoder.encode(graphQLData)
17+
guard let responseString = String(data: serializedJSON, encoding: .utf8) else {
18+
throw APIError.operationError("Could not get String from data", "", nil)
19+
}
20+
guard let response = responseString as? R else {
21+
throw APIError.operationError("Not of type \(String(describing: R.self))", "", nil)
22+
}
23+
return response
24+
} else if request.responseType == AnyModel.self {
25+
let anyModel = try AnyModel(modelJSON: graphQLData)
26+
let serializedJSON = try encoder.encode(anyModel)
27+
return try decoder.decode(request.responseType, from: serializedJSON)
28+
} else {
29+
let serializedJSON = try encoder.encode(graphQLData)
30+
let responseData = try decoder.decode(request.responseType, from: serializedJSON)
31+
return responseData
32+
}
33+
}
34+
35+
private func valueAtDecodePath(from graphQLData: JSONValue) throws -> JSONValue {
36+
guard let decodePath = request.decodePath else {
37+
return graphQLData
38+
}
39+
40+
guard let model = graphQLData.value(at: decodePath) else {
41+
throw APIError.operationError("Could not retrieve object, given decode path: \(decodePath)", "", nil)
42+
}
43+
44+
return model
45+
}
46+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import Amplify
10+
import AWSPluginsCore
11+
12+
class GraphQLResponseDecoder<R: Decodable> {
13+
14+
let request: GraphQLOperationRequest<R>
15+
var response: Data
16+
let decoder = JSONDecoder()
17+
let encoder = JSONEncoder()
18+
19+
public init(request: GraphQLOperationRequest<R>, response: Data = Data()) {
20+
self.request = request
21+
self.response = response
22+
decoder.dateDecodingStrategy = ModelDateFormatting.decodingStrategy
23+
encoder.dateEncodingStrategy = ModelDateFormatting.encodingStrategy
24+
}
25+
26+
func appendResponse(_ data: Data) {
27+
response.append(data)
28+
}
29+
30+
func decodeToGraphQLResponse() throws -> GraphQLResponse<R> {
31+
let appSyncGraphQLResponse = try AWSAppSyncGraphQLResponse.decodeToAWSAppSyncGraphQLResponse(response: response)
32+
switch appSyncGraphQLResponse {
33+
case .data(let data):
34+
return try decodeData(data)
35+
case .errors(let errors):
36+
return try decodeErrors(errors)
37+
case .partial(let data, let errors):
38+
return try decodePartial(graphQLData: data, graphQLErrors: errors)
39+
case .invalidResponse:
40+
guard let rawGraphQLResponseString = String(data: response, encoding: .utf8) else {
41+
throw APIError.operationError(
42+
"Could not get the String representation of the GraphQL response", "")
43+
}
44+
throw APIError.unknown("The service returned some data without any `data` and `errors`",
45+
"""
46+
The service did not return an expected GraphQL response: \
47+
\(rawGraphQLResponseString)
48+
""")
49+
}
50+
}
51+
52+
func decodeData(_ graphQLData: [String: JSONValue]) throws -> GraphQLResponse<R> {
53+
do {
54+
let responseData = try decodeToResponseType(graphQLData)
55+
return GraphQLResponse<R>.success(responseData)
56+
} catch let decodingError as DecodingError {
57+
let error = APIError(error: decodingError)
58+
guard let rawGraphQLResponseString = String(data: response, encoding: .utf8) else {
59+
throw APIError.operationError(
60+
"Could not get the String representation of the GraphQL response", "")
61+
}
62+
return GraphQLResponse<R>.failure(.transformationError(rawGraphQLResponseString, error))
63+
} catch {
64+
throw error
65+
}
66+
}
67+
68+
func decodeErrors(_ graphQLErrors: [JSONValue]) throws -> GraphQLResponse<R> {
69+
let responseErrors = try GraphQLErrorDecoder.decodeErrors(graphQLErrors: graphQLErrors)
70+
return GraphQLResponse<R>.failure(.error(responseErrors))
71+
}
72+
73+
func decodePartial(graphQLData: [String: JSONValue],
74+
graphQLErrors: [JSONValue]) throws -> GraphQLResponse<R> {
75+
do {
76+
if let first = graphQLData.first, case .null = first.value {
77+
let responseErrors = try GraphQLErrorDecoder.decodeErrors(graphQLErrors: graphQLErrors)
78+
return GraphQLResponse<R>.failure(.error(responseErrors))
79+
}
80+
let responseData = try decodeToResponseType(graphQLData)
81+
let responseErrors = try GraphQLErrorDecoder.decodeErrors(graphQLErrors: graphQLErrors)
82+
return GraphQLResponse<R>.failure(.partial(responseData, responseErrors))
83+
} catch let decodingError as DecodingError {
84+
let error = APIError(error: decodingError)
85+
guard let rawGraphQLResponseString = String(data: response, encoding: .utf8) else {
86+
throw APIError.operationError(
87+
"Could not get the String representation of the GraphQL response", "")
88+
}
89+
return GraphQLResponse<R>.failure(.transformationError(rawGraphQLResponseString, error))
90+
} catch {
91+
throw error
92+
}
93+
}
94+
}

AmplifyPlugins/API/AWSAPICategoryPlugin/Support/Internal/AWSAppSyncGraphQLResponse.swift

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,81 @@
88
import Amplify
99

1010
/// The raw response coming back from the AppSync GraphQL service
11-
struct AWSAppSyncGraphQLResponse {
12-
let data: [String: JSONValue]?
13-
let errors: [JSONValue]?
11+
enum AWSAppSyncGraphQLResponse {
12+
case data(_ graphQLData: [String: JSONValue])
13+
case errors(_ graphQLErrors: [JSONValue])
14+
case partial(graphQLData: [String: JSONValue], graphQLErrors: [JSONValue])
15+
case invalidResponse
16+
17+
static func decodeToAWSAppSyncGraphQLResponse(response: Data) throws -> AWSAppSyncGraphQLResponse {
18+
let jsonObject = try deserializeObject(graphQLResponse: response)
19+
do {
20+
let errors = try getAPIErrors(from: jsonObject)
21+
let data = try getGraphQLData(from: jsonObject)
22+
switch (data, errors) {
23+
case (nil, nil):
24+
return .invalidResponse
25+
case (.some(let data), .none):
26+
return .data(data)
27+
case (.none, .some(let errors)):
28+
return .errors(errors)
29+
case (.some(let data), .some(let errors)):
30+
return .partial(graphQLData: data, graphQLErrors: errors)
31+
}
32+
} catch is APIError {
33+
return .invalidResponse
34+
} catch {
35+
throw error
36+
}
37+
}
38+
39+
private static func deserializeObject(graphQLResponse: Data) throws -> [String: JSONValue] {
40+
let json: JSONValue
41+
42+
do {
43+
let decoder = JSONDecoder()
44+
decoder.dateDecodingStrategy = ModelDateFormatting.decodingStrategy
45+
json = try decoder.decode(JSONValue.self, from: graphQLResponse)
46+
} catch {
47+
throw APIError.operationError("Could not decode to JSONValue from the GraphQL Response",
48+
"Service issue",
49+
error)
50+
}
51+
52+
guard case .object(let jsonObject) = json else {
53+
throw APIError.unknown("The GraphQL response is not an object",
54+
"The AppSync service returned a malformed GraphQL response")
55+
}
56+
57+
return jsonObject
58+
}
59+
60+
private static func getAPIErrors(from jsonObject: [String: JSONValue]) throws -> [JSONValue]? {
61+
guard let errors = jsonObject["errors"] else {
62+
return nil
63+
}
64+
65+
guard case .array(let errorArray) = errors else {
66+
throw APIError.unknown("The GraphQL response containing errors should be an array",
67+
"The AppSync service returned a malformed GraphQL response")
68+
}
69+
70+
return errorArray
71+
}
72+
73+
private static func getGraphQLData(from jsonObject: [String: JSONValue]) throws -> [String: JSONValue]? {
74+
guard let data = jsonObject["data"] else {
75+
return nil
76+
}
77+
78+
switch data {
79+
case .object(let dataObject):
80+
return dataObject
81+
case .null:
82+
return nil
83+
default:
84+
throw APIError.unknown("Failed to get object or null from data.",
85+
"The AppSync service returned a malformed GraphQL response")
86+
}
87+
}
1488
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Amplify
9+
10+
extension GraphQLRequest {
11+
func toOperationRequest(operationType: GraphQLOperationType) -> GraphQLOperationRequest<R> {
12+
return GraphQLOperationRequest<R>(apiName: apiName,
13+
operationType: operationType,
14+
document: document,
15+
variables: variables,
16+
responseType: responseType,
17+
decodePath: decodePath,
18+
options: GraphQLOperationRequest.Options())
19+
}
20+
}

0 commit comments

Comments
 (0)