diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index d7c4dc8fee..25793d5b19 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -105,6 +105,8 @@ 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */; }; E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86D8E03214B32DA0028EFE1 /* JSONTests.swift */; }; F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */; }; + F82E62E122BCD223000C311B /* AutomaticPersistedQueriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82E62E022BCD223000C311B /* AutomaticPersistedQueriesTests.swift */; }; + F8AB781B22E1B4BB00A50B81 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8AB781A22E1B4BB00A50B81 /* MockURLSession.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -363,6 +365,9 @@ 9FF90A6C1DDDEB420034C3B6 /* ParseQueryResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseQueryResponseTests.swift; sourceTree = ""; }; E86D8E03214B32DA0028EFE1 /* JSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONTests.swift; sourceTree = ""; }; F16D083B21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryFromJSONBuildingTests.swift; sourceTree = ""; }; + F82E62E022BCD223000C311B /* AutomaticPersistedQueriesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutomaticPersistedQueriesTests.swift; sourceTree = ""; }; + F8AB781A22E1B4BB00A50B81 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; + F8E9D8AE22B2492C0065DA98 /* schema.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = schema.json; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -483,6 +488,7 @@ 9F8A95811EC0FD3300304A2D /* XCTAssertHelpers.swift */, 9F8A95831EC0FD6100304A2D /* XCTestCase+Promise.swift */, 9F10A51D1EC1BA0F0045E62B /* MockNetworkTransport.swift */, + F8AB781A22E1B4BB00A50B81 /* MockURLSession.swift */, 9F8A95851EC0FD9800304A2D /* TestCacheProvider.swift */, 9F8A957A1EC0FC1200304A2D /* ApolloTestSupport.h */, 9F8A957B1EC0FC1200304A2D /* Info.plist */, @@ -651,6 +657,7 @@ 9FCE2CF41E6C20E000E34457 /* Tests */ = { isa = PBXGroup; children = ( + F82E62DC22BCD1F2000C311B /* Apollo */, 9FC750521D2A532D00458D91 /* ApolloTests */, 9FA6ABBD1EC0A988000017BE /* ApolloCacheDependentTests */, 9FC631341E6AE2080062707E /* ApolloPerformanceTests */, @@ -681,6 +688,7 @@ 9FCE2D171E6C259B00E34457 /* Starship.graphql */, 9FCE2D0C1E6C259B00E34457 /* CreateReviewForEpisode.graphql */, 9FCE2D0A1E6C258A00E34457 /* API.swift */, + F8E9D8AE22B2492C0065DA98 /* schema.json */, 9FCE2CFC1E6C213D00E34457 /* StarWarsAPI.h */, 9FCE2CFD1E6C213D00E34457 /* Info.plist */, ); @@ -711,6 +719,22 @@ name = "Supporting Files"; sourceTree = ""; }; + F82E62DC22BCD1F2000C311B /* Apollo */ = { + isa = PBXGroup; + children = ( + F82E62DD22BCD1F2000C311B /* Network */, + ); + path = Apollo; + sourceTree = ""; + }; + F82E62DD22BCD1F2000C311B /* Network */ = { + isa = PBXGroup; + children = ( + F82E62E022BCD223000C311B /* AutomaticPersistedQueriesTests.swift */, + ); + path = Network; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1038,7 +1062,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Do some magic so we can make sure `FRAMEWORK_SEARCH_PATHS` works correctly when there's a space in the scheme or the folder name.\nQUOTED_FRAMEWORK_SEARCH_PATHS=\\\"$(echo $FRAMEWORK_SEARCH_PATHS | tr -d '\"' | sed -e 's/ \\//\" \"\\//g')\\\"\n\n# Get the first result searching for the framework\nAPOLLO_FRAMEWORK_PATH=\"$(eval find ${QUOTED_FRAMEWORK_SEARCH_PATHS} -name \"Apollo.framework\" -maxdepth 1 -print | head -n 1)\"\n\nif [ -z \"${APOLLO_FRAMEWORK_PATH}\" ]; then\n echo \"error: Couldn't find Apollo.framework in FRAMEWORK_SEARCH_PATHS; make sure to add the framework to your project.\"\n exit 1\nfi\n\ncd ${SRCROOT}/Tests/GitHubAPI\nif [[ $SDK_NAME == *\"macos\"* ]]; then\n # Run the mac script\n \"${APOLLO_FRAMEWORK_PATH}\"/Versions/Current/Resources/check-and-run-apollo-cli.sh codegen:generate --queries=\"$(find . -name '*.graphql')\" --schema=schema.json --mergeInFieldsFromFragmentSpreads API.swift\nelse\n # Run the non-mac script\n\"${APOLLO_FRAMEWORK_PATH}\"/check-and-run-apollo-cli.sh codegen:generate --queries=\"$(find . -name '*.graphql')\" --schema=schema.json --mergeInFieldsFromFragmentSpreads API.swift\nfi\n"; + shellScript = "# Do some magic so we can make sure `FRAMEWORK_SEARCH_PATHS` works correctly when there's a space in the scheme or the folder name.\nQUOTED_FRAMEWORK_SEARCH_PATHS=\\\"$(echo $FRAMEWORK_SEARCH_PATHS | tr -d '\"' | sed -e 's/ \\//\" \"\\//g')\\\"\n\n# Get the first result searching for the framework\nAPOLLO_FRAMEWORK_PATH=\"$(eval find ${QUOTED_FRAMEWORK_SEARCH_PATHS} -name \"Apollo.framework\" -maxdepth 1 -print | head -n 1)\"\n\nif [ -z \"${APOLLO_FRAMEWORK_PATH}\" ]; then\n echo \"error: Couldn't find Apollo.framework in FRAMEWORK_SEARCH_PATHS; make sure to add the framework to your project.\"\n exit 1\nfi\n\ncd ${SRCROOT}/Tests/GitHubAPI\nif [[ $SDK_NAME == *\"macos\"* ]]; then\n # Run the mac script\n \"${APOLLO_FRAMEWORK_PATH}\"/Versions/Current/Resources/check-and-run-apollo-cli.sh codegen:generate --queries=\"$(find . -name '*.graphql')\" --schema=schema.json --mergeInFieldsFromFragmentSpreads API.swift\nelse\n # Run the non-mac script\n\"${APOLLO_FRAMEWORK_PATH}\"/check-and-run-apollo-cli.sh codegen:generate --queries=\"$(find . -name '*.graphql')\" --schema=schema.json --operationIdsPath=operationIdsPath.json --mergeInFieldsFromFragmentSpreads API.swift\nfi\n"; }; 9FCE2D061E6C251100E34457 /* Generate Apollo Client API */ = { isa = PBXShellScriptBuildPhase; @@ -1052,7 +1076,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Do some magic so we can make sure `FRAMEWORK_SEARCH_PATHS` works correctly when there's a space in the scheme or the folder name.\nQUOTED_FRAMEWORK_SEARCH_PATHS=\\\"$(echo $FRAMEWORK_SEARCH_PATHS | tr -d '\"' | sed -e 's/ \\//\" \"\\//g')\\\"\n\n# Get the first result searching for the framework\nAPOLLO_FRAMEWORK_PATH=\"$(eval find ${QUOTED_FRAMEWORK_SEARCH_PATHS} -name \"Apollo.framework\" -maxdepth 1 -print | head -n 1)\"\n\nif [ -z \"${APOLLO_FRAMEWORK_PATH}\" ]; then\n echo \"error: Couldn't find Apollo.framework in FRAMEWORK_SEARCH_PATHS; make sure to add the framework to your project.\"\n exit 1\nfi\n\ncd ${SRCROOT}/Tests/StarWarsAPI\nif [[ $SDK_NAME == *\"macos\"* ]]; then\n # Run the mac script\n \"${APOLLO_FRAMEWORK_PATH}\"/Versions/Current/Resources/check-and-run-apollo-cli.sh codegen:generate --queries=\"$(find . -name '*.graphql')\" --schema=schema.json --mergeInFieldsFromFragmentSpreads API.swift\nelse\n # run the non-mac script\n \"${APOLLO_FRAMEWORK_PATH}\"/check-and-run-apollo-cli.sh codegen:generate --queries=\"$(find . -name '*.graphql')\" --schema=schema.json --mergeInFieldsFromFragmentSpreads API.swift\nfi\n"; + shellScript = "# Do some magic so we can make sure `FRAMEWORK_SEARCH_PATHS` works correctly when there's a space in the scheme or the folder name.\nQUOTED_FRAMEWORK_SEARCH_PATHS=\\\"$(echo $FRAMEWORK_SEARCH_PATHS | tr -d '\"' | sed -e 's/ \\//\" \"\\//g')\\\"\n\n# Get the first result searching for the framework\nAPOLLO_FRAMEWORK_PATH=\"$(eval find ${QUOTED_FRAMEWORK_SEARCH_PATHS} -name \"Apollo.framework\" -maxdepth 1 -print | head -n 1)\"\n\nif [ -z \"${APOLLO_FRAMEWORK_PATH}\" ]; then\n echo \"error: Couldn't find Apollo.framework in FRAMEWORK_SEARCH_PATHS; make sure to add the framework to your project.\"\n exit 1\nfi\n\ncd ${SRCROOT}/Tests/StarWarsAPI\nif [[ $SDK_NAME == *\"macos\"* ]]; then\n # Run the mac script\n \"${APOLLO_FRAMEWORK_PATH}\"/Versions/Current/Resources/check-and-run-apollo-cli.sh codegen:generate --queries=\"$(find . -name '*.graphql')\" --schema=schema.json --mergeInFieldsFromFragmentSpreads API.swift\nelse\n # run the non-mac script\n \"${APOLLO_FRAMEWORK_PATH}\"/check-and-run-apollo-cli.sh codegen:generate --queries=\"$(find . -name '*.graphql')\" --schema=schema.json --operationIdsPath=operationIdsPath.json --mergeInFieldsFromFragmentSpreads API.swift\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -1064,6 +1088,7 @@ 9F8A95841EC0FD6100304A2D /* XCTestCase+Promise.swift in Sources */, 9F8A95821EC0FD3300304A2D /* XCTAssertHelpers.swift in Sources */, 9F10A51E1EC1BA0F0045E62B /* MockNetworkTransport.swift in Sources */, + F8AB781B22E1B4BB00A50B81 /* MockURLSession.swift in Sources */, 9F8A95861EC0FD9800304A2D /* TestCacheProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1151,6 +1176,7 @@ files = ( 9FC9A9C81E2EFE6E0023C4D5 /* CacheKeyForFieldTests.swift in Sources */, 9F91CF8F1F6C0DB2008DD0BE /* MutatingResultsTests.swift in Sources */, + F82E62E122BCD223000C311B /* AutomaticPersistedQueriesTests.swift in Sources */, 9F19D8461EED8D3B00C57247 /* ResultOrPromiseTests.swift in Sources */, 9F533AB31E6C4A4200CBE097 /* BatchedLoadTests.swift in Sources */, 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */, diff --git a/Sources/Apollo/GraphQLGETTransformer.swift b/Sources/Apollo/GraphQLGETTransformer.swift index 61aa91f35d..a899f2ce56 100644 --- a/Sources/Apollo/GraphQLGETTransformer.swift +++ b/Sources/Apollo/GraphQLGETTransformer.swift @@ -13,9 +13,6 @@ struct GraphQLGETTransformer { let body: GraphQLMap let url: URL - private let variablesKey = "variables" - private let queryKey = "query" - /// A helper for transforming a GraphQLMap that can be sent with a `POST` request into a URL with query parameters for a `GET` request. /// /// - Parameters: @@ -35,26 +32,23 @@ struct GraphQLGETTransformer { return nil } - guard let query = self.body.jsonObject[self.queryKey] as? String else { + var queryItems: [URLQueryItem] = [] + + do { + _ = try body.sorted(by: {$0.key < $1.key}).compactMap({ arg in + if let value = arg.value as? GraphQLMap { + let data = try JSONSerialization.dataSortedIfPossible(withJSONObject: value.jsonValue) + if let string = String(data: data, encoding: .utf8) { + queryItems.append(URLQueryItem(name: arg.key, value: string)) + } + } else if let string = arg.value as? String { + queryItems.append(URLQueryItem(name: arg.key, value: string)) + } + }) + } catch { return nil } - - var queryItems = components.queryItems ?? [URLQueryItem]() - queryItems.append(URLQueryItem(name: self.queryKey, value: query)) - components.queryItems = queryItems - - guard let variables = self.body.jsonObject[self.variablesKey] as? [String: Any] else { - return components.url - } - - guard - let serializedData = try? JSONSerialization.dataSortedIfPossible(withJSONObject: variables), - let jsonString = String(bytes: serializedData, encoding: .utf8) else { - return components.url - } - - queryItems.append(URLQueryItem(name: self.variablesKey, value: jsonString)) components.queryItems = queryItems return components.url diff --git a/Sources/Apollo/GraphQLHTTPResponseError.swift b/Sources/Apollo/GraphQLHTTPResponseError.swift index c3825f6ced..82add345c0 100644 --- a/Sources/Apollo/GraphQLHTTPResponseError.swift +++ b/Sources/Apollo/GraphQLHTTPResponseError.swift @@ -5,6 +5,8 @@ public struct GraphQLHTTPResponseError: Error, LocalizedError { public enum ErrorKind { case errorResponse case invalidResponse + case persistedQueryNotFound + case persistedQueryNotSupported var description: String { switch self { @@ -12,6 +14,10 @@ public struct GraphQLHTTPResponseError: Error, LocalizedError { return "Received error response" case .invalidResponse: return "Received invalid response" + case .persistedQueryNotFound: + return "Persisted query not found" + case .persistedQueryNotSupported: + return "Persisted query not supported" } } } diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift index 7272cd14c6..a0c97b7074 100644 --- a/Sources/Apollo/HTTPNetworkTransport.swift +++ b/Sources/Apollo/HTTPNetworkTransport.swift @@ -69,30 +69,34 @@ public protocol HTTPNetworkTransportRetryDelegate: HTTPNetworkTransportDelegate /// A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation. public class HTTPNetworkTransport: NetworkTransport { + let url: URL - let session: URLSession + var session: URLSession let serializationFormat = JSONSerializationFormat.self - let useGETForQueries: Bool let delegate: HTTPNetworkTransportDelegate? + private let useGETForQueries: Bool + private let enableAutoPersistedQueries: Bool + private let useGETForPersistedQueryRetry: Bool + /// Creates a network transport with the specified server URL and session configuration. /// /// - Parameters: /// - url: The URL of a GraphQL server to connect to. /// - configuration: A session configuration used to configure the session. Defaults to `URLSessionConfiguration.default`. - /// - sendOperationIdentifiers: Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false. /// - useGETForQueries: If query operation should be sent using GET instead of POST. Defaults to false. - /// - delegate: [Optional] A delegate which can conform to any or all of `HTTPNetworkTransportPreflightDelegate`, `HTTPNetworkTransportTaskCompletedDelegate`, and `HTTPNetworkTransportRetryDelegate`. Defaults to nil. - public init(url: URL, - configuration: URLSessionConfiguration = .default, - sendOperationIdentifiers: Bool = false, - useGETForQueries: Bool = false, - delegate: HTTPNetworkTransportDelegate? = nil) { + /// - enableAutoPersistedQueries: Whether to send persistedQuery extension. QueryDocument will be absent at 1st request, retry with QueryDocument if server respond PersistedQueryNotFound or PersistedQueryNotSupport. Defaults to false. + /// - useGETForPersistedQueryRetry: Whether to retry persistedQuery request with HttpGetMethod. + /// - preflightDelegate: A delegate to check with before sending a request. + /// - requestCompletionDelegate: A delegate to notify when the URLSessionTask has completed. + public init(url: URL, configuration: URLSessionConfiguration = URLSessionConfiguration.default, useGETForQueries: Bool = false, enableAutoPersistedQueries: Bool = false, useGETForPersistedQueryRetry: Bool = false, delegate: HTTPNetworkTransportDelegate? = nil + ) { self.url = url self.session = URLSession(configuration: configuration) - self.sendOperationIdentifiers = sendOperationIdentifiers - self.useGETForQueries = useGETForQueries self.delegate = delegate + self.useGETForQueries = useGETForQueries + self.enableAutoPersistedQueries = enableAutoPersistedQueries + self.useGETForPersistedQueryRetry = useGETForPersistedQueryRetry } /// Send a GraphQL operation to a server and return a response. @@ -104,110 +108,160 @@ public class HTTPNetworkTransport: NetworkTransport { /// - error: An error that indicates why a request failed, or `nil` if the request was succesful. /// - Returns: An object that can be used to cancel an in progress request. public func send(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) -> Cancellable { + return send(operation: operation, retryFor: nil, completionHandler: completionHandler) + } + + private func send(operation: Operation, retryFor reason:Error? = nil, completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) -> Cancellable { + let request: URLRequest do { - request = try self.createRequest(for: operation) + if operation.operationType != .query { + request = try self.createRequest(for: operation, + httpMethod: .POST, + sendQueryDocument: true, + autoPersistQueries: false) + } else { + let useGetMethod: Bool + let sendQueryDocument: Bool + if let reason = reason as? GraphQLHTTPResponseError, + [GraphQLHTTPResponseError.ErrorKind.persistedQueryNotFound, + GraphQLHTTPResponseError.ErrorKind.persistedQueryNotSupported].contains(reason.kind) { + // retry for APQs, with document + useGetMethod = useGETForQueries + sendQueryDocument = true + } else { + useGetMethod = useGETForQueries || (enableAutoPersistedQueries && useGETForPersistedQueryRetry) + sendQueryDocument = !enableAutoPersistedQueries + } + + request = try self.createRequest(for: operation, + httpMethod: useGetMethod ? .GET : .POST, + sendQueryDocument: sendQueryDocument, + autoPersistQueries: enableAutoPersistedQueries) + } } catch { completionHandler(nil, error) return EmptyCancellable() } let task = session.dataTask(with: request) { [weak self] data, response, error in - self?.rawTaskCompleted(request: request, - data: data, - response: response, - error: error) - + guard let self = self else { return } + + self.rawTaskCompleted(request: request, + data: data, + response: response, + error: error) + if let receivedError = error { - self?.handleErrorOrRetry(operation: operation, - error: receivedError, - for: request, - response: response, - completionHandler: completionHandler) + self.handleErrorOrRetry(operation: operation, + error: receivedError, + for: request, + response: response, + completionHandler: completionHandler) return } - + guard let httpResponse = response as? HTTPURLResponse else { fatalError("Response should be an HTTPURLResponse") } - + guard httpResponse.isSuccessful else { let unsuccessfulError = GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .errorResponse) - self?.handleErrorOrRetry(operation: operation, - error: unsuccessfulError, - for: request, - response: response, - completionHandler: completionHandler) + self.handleErrorOrRetry(operation: operation, + error: unsuccessfulError, + for: request, + response: response, + completionHandler: completionHandler) return } - + guard let data = data else { let error = GraphQLHTTPResponseError(body: nil, response: httpResponse, kind: .invalidResponse) - self?.handleErrorOrRetry(operation: operation, - error: error, - for: request, - response: response, - completionHandler: completionHandler) + self.handleErrorOrRetry(operation: operation, + error: error, + for: request, + response: response, + completionHandler: completionHandler) return } - + do { - guard let body = try self?.serializationFormat.deserialize(data: data) as? JSONObject else { + guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else { throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse) } - let response = GraphQLResponse(operation: operation, body: body) - completionHandler(response, nil) + if self.enableAutoPersistedQueries, + let error = body["errors"] as? [JSONObject], + let errorMsg = error.filter ({ $0["message"] as? String != nil }).first?["message"] as? String, + ["PersistedQueryNotFound","PersistedQueryNotSupported"].contains(errorMsg) { + + let errorKind: GraphQLHTTPResponseError.ErrorKind = (errorMsg == "PersistedQueryNotFound") ? .persistedQueryNotFound : .persistedQueryNotSupported + let apqsError = GraphQLHTTPResponseError(body: data, + response: httpResponse, + kind: errorKind) + + let retryByDefault: Bool + if let reason = reason as? GraphQLHTTPResponseError, + reason.kind == apqsError.kind { + // if the same error occurs, respect retry option from delegate + retryByDefault = false + } else { + retryByDefault = true + } + // Auto Persisted Query handling + self.handleErrorOrRetry(operation: operation, + error: apqsError, + for: request, + response: response, + retryByDefault: retryByDefault, + completionHandler: completionHandler) + + }else { + // no errors + let response = GraphQLResponse(operation: operation, body: body) + completionHandler(response, nil) + } } catch let parsingError { - self?.handleErrorOrRetry(operation: operation, - error: parsingError, - for: request, - response: response, - completionHandler: completionHandler) + self.handleErrorOrRetry(operation: operation, + error: parsingError, + for: request, + response: response, + completionHandler: completionHandler) + } } - task.resume() - return task } - - private let sendOperationIdentifiers: Bool - private func handleErrorOrRetry(operation: Operation, - error: Error, - for request: URLRequest, - response: URLResponse?, - completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) { - guard - let delegate = self.delegate, - let retrier = delegate as? HTTPNetworkTransportRetryDelegate else { + // retryByDefault: still retry if delegate is absent, otherwise repect the value from delegate + private func handleErrorOrRetry(operation: Operation, error: Error, for request: URLRequest, response: URLResponse?, retryByDefault: Bool = false, completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) { + if let delegate = self.delegate, + let retrier = delegate as? HTTPNetworkTransportRetryDelegate { + retrier.networkTransport( + self, + receivedError: error, + for: request, + response: response, + retryHandler: { [weak self] shouldRetry in + guard let self = self, shouldRetry else { + completionHandler(nil, error) + return + } + _ = self.send(operation: operation, retryFor: error, completionHandler: completionHandler) + }) + } else if retryByDefault { + _ = self.send(operation: operation, retryFor: error, completionHandler: completionHandler) + } else { completionHandler(nil, error) return } - - retrier.networkTransport( - self, - receivedError: error, - for: request, - response: response, - retryHandler: { [weak self] shouldRetry in - guard shouldRetry else { - completionHandler(nil, error) - return - } - - _ = self?.send(operation: operation, completionHandler: completionHandler) - }) } - - private func rawTaskCompleted(request: URLRequest, - data: Data?, - response: URLResponse?, - error: Error?) { + + private func rawTaskCompleted(request: URLRequest, data: Data?, response: URLResponse?, error: Error?) { guard let delegate = self.delegate, let taskDelegate = delegate as? HTTPNetworkTransportTaskCompletedDelegate else { @@ -221,11 +275,12 @@ public class HTTPNetworkTransport: NetworkTransport { error: error) } - private func createRequest(for operation: Operation) throws -> URLRequest { - let body = requestBody(for: operation) - var request = URLRequest(url: self.url) + private func createRequest(for operation: Operation, httpMethod: GraphQLHTTPMethod, sendQueryDocument: Bool, autoPersistQueries: Bool) throws -> URLRequest { - if self.useGETForQueries && operation.operationType == .query { + var request: URLRequest + let body = requestBody(for: operation, sendQueryDocument: sendQueryDocument, autoPersistQueries: autoPersistQueries) + + if httpMethod == .GET { let transformer = GraphQLGETTransformer(body: body, url: self.url) if let urlForGet = transformer.createGetURL() { request = URLRequest(url: urlForGet) @@ -235,14 +290,16 @@ public class HTTPNetworkTransport: NetworkTransport { } } else { do { - request.httpBody = try serializationFormat.serialize(value: body) + request = URLRequest(url: self.url) request.httpMethod = GraphQLHTTPMethod.POST.rawValue + request.httpBody = try serializationFormat.serialize(value: body) } catch { throw GraphQLHTTPRequestError.serializedBodyMessageError } } - + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(operation.operationIdentifier, forHTTPHeaderField: "X-APOLLO-OPERATION-ID") // If there's a delegate, do a pre-flight check and allow modifications to the request. if @@ -258,15 +315,31 @@ public class HTTPNetworkTransport: NetworkTransport { return request } - private func requestBody(for operation: Operation) -> GraphQLMap { - if sendOperationIdentifiers { + private func requestBody(for operation: Operation, + sendQueryDocument: Bool, + autoPersistQueries: Bool) -> GraphQLMap { + + var payload: GraphQLMap = [:] + + if autoPersistQueries { guard let operationIdentifier = operation.operationIdentifier else { - preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers") + preconditionFailure("To enabled autoPersistQueries, Apollo types must be generated with operationIdentifiers") } - - return ["id": operationIdentifier, "variables": operation.variables] + payload["extensions"] = [ + "persistedQuery" : ["sha256Hash": operationIdentifier, "version": 1] + ] + } + + if let variables = operation.variables?.compactMapValues({ $0 }), variables.count > 0 { + payload["variables"] = variables + } + + if sendQueryDocument { + // TODO: This work-around fix "operationId is invalid for swift codegen" (https://github.com/apollographql/apollo-tooling/issues/1362), please remove this work-around after it's fixed. + let modifiedQuery = operation.queryDocument.replacingOccurrences(of: "fragment", with: "\nfragment") + payload["query"] = modifiedQuery } - return ["query": operation.queryDocument, "variables": operation.variables] + return payload } } diff --git a/Tests/Apollo/Network/AutomaticPersistedQueriesTests.swift b/Tests/Apollo/Network/AutomaticPersistedQueriesTests.swift new file mode 100644 index 0000000000..4ad949ac51 --- /dev/null +++ b/Tests/Apollo/Network/AutomaticPersistedQueriesTests.swift @@ -0,0 +1,343 @@ +import XCTest +@testable import Apollo +import ApolloTestSupport +import StarWarsAPI + +class AutomaticPersistedQueriesTests: XCTestCase { + + private final let endpoint = "http://localhost:8080/graphql" + + func testRequestBody() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery() + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.httpMethod, "POST") + + validatePostBody(with: request, + query: query, + queryDocument: true) + } + + func testRequestBodyWithVariable() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery(episode: .jedi) + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.httpMethod, "POST") + + validatePostBody(with: request, + query: query, + queryDocument: true, + variable: true) + } + + + func testRequestBodyForAPQsWithVariable() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + enableAutoPersistedQueries: true) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery(episode: .empire) + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.httpMethod, "POST") + + validatePostBody(with: request, + query: query, + persistedQuery: true, + variable: true) + } + + func testQueryStringForAPQsUseGetMethod() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + enableAutoPersistedQueries: true, + useGETForPersistedQueryRetry: true) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery() + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + + validateUrlParams(with: request, + query: query, + persistedQuery: true) + } + + func testQueryStringForAPQsUseGetMethodWithVariable() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + enableAutoPersistedQueries: true, + useGETForPersistedQueryRetry: true) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery(episode: .empire) + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.httpMethod, "GET") + + validateUrlParams(with: request, + query: query, + persistedQuery: true, + variable: true) + } + + func testUseGETForQueriesRequest() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + useGETForQueries: true) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery() + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.httpMethod, "GET") + + validateUrlParams(with: request, + query: query, + queryDocument: true) + } + + func testNotUseGETForQueriesRequest() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery() + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.httpMethod, "POST") + + validatePostBody(with: request, + query: query, + queryDocument: true) + } + + func testNotUseGETForQueriesAPQsRequest() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + enableAutoPersistedQueries: true) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery(episode: .empire) + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.httpMethod, "POST") + + validatePostBody(with: request, + query: query, + persistedQuery: true, + variable: true) + } + + func testUseGETForQueriesAPQsRequest() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + useGETForQueries: true, + enableAutoPersistedQueries: true) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery(episode: .empire) + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.httpMethod, "GET") + + validateUrlParams(with: request, + query: query, + persistedQuery: true, + variable: true) + } + + func testNotUseGETForQueriesAPQsGETRequest() { + let network = HTTPNetworkTransport(url: URL(string: endpoint)!, + enableAutoPersistedQueries: true, + useGETForPersistedQueryRetry: true) + let mockSession = MockURLSession() + network.session = mockSession + let query = HeroNameQuery(episode: .empire) + let _ = network.send(operation: query) { _,_ in } + + guard let request = mockSession.lastRequest else { + XCTFail("last request should not be nil") + return + } + XCTAssertEqual(request.url?.host, network.url.host) + XCTAssertEqual(request.httpMethod, "GET") + + validateUrlParams(with: request, + query: query, + persistedQuery: true, + variable: true) + } +} + +// MARK: Helpers + +private func validateUrlParams(with request: URLRequest, query: HeroNameQuery, queryDocument: Bool = false, persistedQuery: Bool = false, variable: Bool = false) { + guard let url = request.url else { + XCTFail("URL not valid") + return + } + + let queryStting = url.queryItems?["query"] + if queryDocument { + XCTAssertEqual(queryStting, query.queryDocument) + } else { + XCTAssertNil(queryStting) + } + + let variables = url.queryItems?["variables"] + if variable { + XCTAssertNotNil(variables) + XCTAssertEqual(variables, "{\"episode\":\"\(query.episode?.rawValue ?? "")\"}") + }else{ + XCTAssertNil(variables) + } + + let ext = url.queryItems?["extensions"] + if persistedQuery { + guard let ext = ext, + let data = ext.data(using: .utf8), + let jsonBody = try? JSONSerializationFormat.deserialize(data: data) as? JSONObject + else { + XCTFail("extensions json data should not be nil") + return + } + + guard let persistedQuery = jsonBody["persistedQuery"] as? JSONObject else { + XCTFail("persistedQuery is missing") + return + } + + guard let sha256Hash = persistedQuery["sha256Hash"] as? String else { + XCTFail("sha256Hash is missing") + return + } + + guard let version = persistedQuery["version"] as? Int else { + XCTFail("version is missing") + return + } + + XCTAssertEqual(version, 1) + XCTAssertEqual(sha256Hash, query.operationIdentifier) + } else { + XCTAssertNil(ext) + } +} + +private func validatePostBody(with request: URLRequest, query: HeroNameQuery, queryDocument: Bool = false, persistedQuery: Bool = false, variable: Bool = false) { + + guard let httpBody = request.httpBody, + let jsonBody = try? JSONSerializationFormat.deserialize(data: httpBody) as? JSONObject else { + XCTFail("httpBody invalid") + return + } + + let queryStting = jsonBody["query"] as? String + if queryDocument { + XCTAssertEqual(queryStting, query.queryDocument) + } + + let variables = jsonBody["variables"] as? JSONObject + if variable { + XCTAssertNotNil(variables) + XCTAssertEqual(variables?["episode"] as? String, query.episode?.rawValue) + }else{ + XCTAssertNil(variables) + } + + let ext = jsonBody["extensions"] as? JSONObject + if persistedQuery { + guard let ext = ext else { + XCTFail("extensions json data should not be nil") + return + } + + guard let persistedQuery = ext["persistedQuery"] as? JSONObject else { + XCTFail("persistedQuery is missing") + return + } + + guard let version = persistedQuery["version"] as? Int else { + XCTFail("version is missing") + return + } + + guard let sha256Hash = persistedQuery["sha256Hash"] as? String else { + XCTFail("sha256Hash is missing") + return + } + + XCTAssertEqual(version, 1) + XCTAssertEqual(sha256Hash, query.operationIdentifier) + } else { + XCTAssertNil(ext) + } +} + +extension URL { + var queryItems: [String: String]? { + return URLComponents(url: self, resolvingAgainstBaseURL: false)? + .queryItems? + .compactMap { $0.dictionaryRepresentation } + .reduce([String:String]()) { dict, tuple in + var dict = dict + tuple.forEach { dict[$0] = $1 } + return dict + } + } +} + +extension URLQueryItem { + var dictionaryRepresentation: [String: String]? { + if let value = value { + return [name: value] + } + return nil + } +} diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index df847ef0ff..7f31f79588 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -3,9 +3,61 @@ import XCTest import ApolloTestSupport import StarWarsAPI + +protocol TestConfig { + func network() -> HTTPNetworkTransport +} + +class DefaultConfig: TestConfig { + func network() -> HTTPNetworkTransport { + return HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) + } +} + +class APQsConfig: TestConfig { + func network() -> HTTPNetworkTransport { + return HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, + enableAutoPersistedQueries: true) + } +} + +class APQsWithGetMethodConfig: TestConfig, HTTPNetworkTransportRetryDelegate{ + func networkTransport(_ networkTransport: HTTPNetworkTransport, receivedError error: Error, for request: URLRequest, response: URLResponse?, retryHandler: @escaping (Bool) -> Void) { + retryHandler(true) + } + + func network() -> HTTPNetworkTransport { + return HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!, + enableAutoPersistedQueries: true, + useGETForPersistedQueryRetry: true, + delegate: self) + } + +} + +class StarWarsServerAPQsGetMethodTests: StarWarsServerTests { + override func setUp() { + super.setUp() + config = APQsWithGetMethodConfig() + } +} + +class StarWarsServerAPQsTests: StarWarsServerTests { + override func setUp() { + super.setUp() + config = APQsConfig() + } +} + class StarWarsServerTests: XCTestCase { // MARK: Queries + var config: TestConfig! + override func setUp() { + super.setUp() + config = DefaultConfig() + } + func testHeroNameQuery() { fetch(query: HeroNameQuery()) { data in XCTAssertEqual(data.hero?.name, "R2-D2") @@ -263,12 +315,12 @@ class StarWarsServerTests: XCTestCase { } // MARK: - Helpers - + private func fetch(query: Query, completionHandler: @escaping (_ data: Query.Data) -> Void) { withCache { (cache) in - let network = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) + let store = ApolloStore(cache: cache) - let client = ApolloClient(networkTransport: network, store: store) + let client = ApolloClient(networkTransport: config.network(), store: store) let expectation = self.expectation(description: "Fetching query") @@ -293,9 +345,9 @@ class StarWarsServerTests: XCTestCase { private func perform(mutation: Mutation, completionHandler: @escaping (_ data: Mutation.Data) -> Void) { withCache { (cache) in - let network = HTTPNetworkTransport(url: URL(string: "http://localhost:8080/graphql")!) + let store = ApolloStore(cache: cache) - let client = ApolloClient(networkTransport: network, store: store) + let client = ApolloClient(networkTransport: config.network(), store: store) let expectation = self.expectation(description: "Performing mutation") diff --git a/Tests/ApolloTestSupport/MockURLSession.swift b/Tests/ApolloTestSupport/MockURLSession.swift new file mode 100644 index 0000000000..7562119a9a --- /dev/null +++ b/Tests/ApolloTestSupport/MockURLSession.swift @@ -0,0 +1,27 @@ +// +// MockURLSession.swift +// ApolloTestSupport +// +// Copyright © 2019 Apollo GraphQL. All rights reserved. +// + +import Foundation + +public final class MockURLSession: URLSession { + public private (set) var lastRequest: URLRequest? + + override public func dataTask(with request: URLRequest) -> URLSessionDataTask { + lastRequest = request + return URLSessionDataTaskMock() + } + + override public func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + lastRequest = request + return URLSessionDataTaskMock() + } +} + +private final class URLSessionDataTaskMock: URLSessionDataTask { + override func resume() { + } +} diff --git a/Tests/ApolloTests/GETTransformerTests.swift b/Tests/ApolloTests/GETTransformerTests.swift index 3f974f5f0c..54a9135a66 100644 --- a/Tests/ApolloTests/GETTransformerTests.swift +++ b/Tests/ApolloTests/GETTransformerTests.swift @@ -25,7 +25,9 @@ class GETTransformerTests: XCTestCase { let url = transformer.createGetURL() - XCTAssertEqual(url?.absoluteString, "http://localhost:8080/graphql?query=query%20HeroName($episode:%20Episode)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:%22EMPIRE%22%7D") + let queryString = url?.absoluteString == "http://localhost:8080/graphql?query=query%20HeroName($episode:%20Episode)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:%22EMPIRE%22%7D" + + XCTAssertTrue(queryString) } func testEncodingQueryWithMoreThanOneParameterIncludingNonHashableValue() { @@ -86,6 +88,133 @@ class GETTransformerTests: XCTestCase { } } + func testEncodingQueryWith2DParameter() { + let operation = HeroNameQuery(episode: .empire) + + let persistedQuery: GraphQLMap = [ + "version": 1, + "sha256Hash": operation.operationIdentifier + ] + + let extensions: GraphQLMap = [ + "persistedQuery": persistedQuery + ] + + let body: GraphQLMap = [ + "query": operation.queryDocument, + "variables": operation.variables, + "extensions": extensions + ] + + let transformer = GraphQLGETTransformer(body: body, url: self.url) + + let url = transformer.createGetURL() + + if #available(iOS 11, macOS 13, watchOS 4, tvOS 11, *) { + let queryString = url?.absoluteString == "http://localhost:8080/graphql?extensions=%7B%22persistedQuery%22:%7B%22sha256Hash%22:%22f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671%22,%22version%22:1%7D%7D&query=query%20HeroName($episode:%20Episode)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:%22EMPIRE%22%7D" + + XCTAssertTrue(queryString) + } else { + guard let query = url?.queryItems?["query"] else { + XCTFail("query should not nil") + return + } + XCTAssertTrue(query == operation.queryDocument) + + guard let variables = url?.queryItems?["variables"] else { + XCTFail("variables should not nil") + return + } + XCTAssertEqual(variables, "{\"episode\":\"EMPIRE\"}") + + guard let ext = url?.queryItems?["extensions"], + let data = ext.data(using: .utf8), + let jsonBody = try? JSONSerializationFormat.deserialize(data: data) as? JSONObject + else { + XCTFail("extensions json data should not be nil") + return + } + + guard let comparePersistedQuery = jsonBody["persistedQuery"] as? JSONObject else { + XCTFail("persistedQuery is missing") + return + } + + guard let sha256Hash = comparePersistedQuery["sha256Hash"] as? String else { + XCTFail("sha256Hash is missing") + return + } + + guard let version = comparePersistedQuery["version"] as? Int else { + XCTFail("version is missing") + return + } + + XCTAssertEqual(version, 1) + XCTAssertEqual(sha256Hash, "f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671") + } + } + + func testEncodingQueryWith2DWOQueryParameter() { + let operation = HeroNameQuery(episode: .empire) + + let persistedQuery: GraphQLMap = [ + "version": 1, + "sha256Hash": operation.operationIdentifier + ] + + let extensions: GraphQLMap = [ + "persistedQuery": persistedQuery + ] + + let body: GraphQLMap = [ + "variables": operation.variables, + "extensions": extensions + ] + + let transformer = GraphQLGETTransformer(body: body, url: self.url) + + let url = transformer.createGetURL() + + if #available(iOS 11, macOS 13, watchOS 4, tvOS 11, *) { + let queryString = url?.absoluteString == "http://localhost:8080/graphql?extensions=%7B%22persistedQuery%22:%7B%22sha256Hash%22:%22f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671%22,%22version%22:1%7D%7D&variables=%7B%22episode%22:%22EMPIRE%22%7D" + XCTAssertTrue(queryString) + } else { + + guard let variables = url?.queryItems?["variables"] else { + XCTFail("variables should not nil") + return + } + XCTAssertEqual(variables, "{\"episode\":\"EMPIRE\"}") + + guard let ext = url?.queryItems?["extensions"], + let data = ext.data(using: .utf8), + let jsonBody = try? JSONSerializationFormat.deserialize(data: data) as? JSONObject + else { + XCTFail("extensions json data should not be nil") + return + } + + guard let comparePersistedQuery = jsonBody["persistedQuery"] as? JSONObject else { + XCTFail("persistedQuery is missing") + return + } + + guard let sha256Hash = comparePersistedQuery["sha256Hash"] as? String else { + XCTFail("sha256Hash is missing") + return + } + + guard let version = comparePersistedQuery["version"] as? Int else { + XCTFail("version is missing") + return + } + + XCTAssertEqual(version, 1) + XCTAssertEqual(sha256Hash, "f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671") + } + } + func testEncodingQueryWithNullDefaultParameter() { let operation = HeroNameQuery() let body: GraphQLMap = [ @@ -96,8 +225,69 @@ class GETTransformerTests: XCTestCase { let transformer = GraphQLGETTransformer(body: body, url: self.url) let url = transformer.createGetURL() + let queryString = url?.absoluteString == "http://localhost:8080/graphql?query=query%20HeroName($episode:%20Episode)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:null%7D" + + XCTAssertTrue(queryString) + } + + func testEncodingQueryWith2DNullDefaultParameter() { + let operation = HeroNameQuery() + + let persistedQuery: GraphQLMap = [ + "version": 1, + "sha256Hash": operation.operationIdentifier + ] + + let extensions: GraphQLMap = [ + "persistedQuery": persistedQuery + ] - XCTAssertEqual(url?.absoluteString, "http://localhost:8080/graphql?query=query%20HeroName($episode:%20Episode)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:null%7D") + let body: GraphQLMap = [ + "query": operation.queryDocument, + "variables": operation.variables, + "extensions": extensions + ] + + let transformer = GraphQLGETTransformer(body: body, url: self.url) + + let url = transformer.createGetURL() + + if #available(iOS 11, macOS 13, watchOS 4, tvOS 11, *) { + let queryString = url?.absoluteString == "http://localhost:8080/graphql?extensions=%7B%22persistedQuery%22:%7B%22sha256Hash%22:%22f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671%22,%22version%22:1%7D%7D&query=query%20HeroName($episode:%20Episode)%20%7B%0A%20%20hero(episode:%20$episode)%20%7B%0A%20%20%20%20__typename%0A%20%20%20%20name%0A%20%20%7D%0A%7D&variables=%7B%22episode%22:null%7D" + XCTAssertTrue(queryString) + } else { + guard let variables = url?.queryItems?["variables"] else { + XCTFail("variables should not nil") + return + } + XCTAssertEqual(variables, "{\"episode\":null}") + + guard let ext = url?.queryItems?["extensions"], + let data = ext.data(using: .utf8), + let jsonBody = try? JSONSerializationFormat.deserialize(data: data) as? JSONObject + else { + XCTFail("extensions json data should not be nil") + return + } + + guard let comparePersistedQuery = jsonBody["persistedQuery"] as? JSONObject else { + XCTFail("persistedQuery is missing") + return + } + + guard let sha256Hash = comparePersistedQuery["sha256Hash"] as? String else { + XCTFail("sha256Hash is missing") + return + } + + guard let version = comparePersistedQuery["version"] as? Int else { + XCTFail("version is missing") + return + } + + XCTAssertEqual(version, 1) + XCTAssertEqual(sha256Hash, "f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671") + } } func testMissingQueryParameterInBodyReturnsNil() { @@ -109,6 +299,6 @@ class GETTransformerTests: XCTestCase { let transformer = GraphQLGETTransformer(body: body, url: self.url) let url = transformer.createGetURL() - XCTAssertNil(url) + XCTAssertEqual(url?.absoluteString, "http://localhost:8080/graphql?variables=%7B%22episode%22:%22EMPIRE%22%7D") } } diff --git a/Tests/ApolloTests/HTTPTransportTests.swift b/Tests/ApolloTests/HTTPTransportTests.swift index 3efb67ed7d..d6db5defdc 100644 --- a/Tests/ApolloTests/HTTPTransportTests.swift +++ b/Tests/ApolloTests/HTTPTransportTests.swift @@ -257,6 +257,9 @@ extension HTTPTransportTests: HTTPNetworkTransportRetryDelegate { retryHandler(true) case .invalidResponse: retryHandler(false) + case .persistedQueryNotFound, + .persistedQueryNotSupported: + retryHandler(false) } } } diff --git a/Tests/GitHubAPI/API.swift b/Tests/GitHubAPI/API.swift index 7cdb48deb9..3adaaf9bf6 100644 --- a/Tests/GitHubAPI/API.swift +++ b/Tests/GitHubAPI/API.swift @@ -6,6 +6,8 @@ public final class RepositoryQuery: GraphQLQuery { public let operationDefinition = "query Repository {\n repository(owner: \"apollographql\", name: \"apollo-ios\") {\n __typename\n issueOrPullRequest(number: 13) {\n __typename\n ... on Issue {\n body\n ... on UniformResourceLocatable {\n url\n }\n author {\n __typename\n avatarUrl\n }\n }\n ... on Reactable {\n viewerCanReact\n ... on Comment {\n author {\n __typename\n login\n }\n }\n }\n }\n }\n}" + public let operationIdentifier: String? = "63e25c339275a65f43b847e692e42caed8c06e25fbfb3dc8db6d4897b180c9ef" + public init() { } diff --git a/Tests/GitHubAPI/operationIdsPath.json b/Tests/GitHubAPI/operationIdsPath.json new file mode 100644 index 0000000000..e8e0c95642 --- /dev/null +++ b/Tests/GitHubAPI/operationIdsPath.json @@ -0,0 +1,6 @@ +{ + "63e25c339275a65f43b847e692e42caed8c06e25fbfb3dc8db6d4897b180c9ef": { + "name": "Repository", + "source": "query Repository {\n repository(owner: \"apollographql\", name: \"apollo-ios\") {\n __typename\n issueOrPullRequest(number: 13) {\n __typename\n ... on Issue {\n body\n ... on UniformResourceLocatable {\n url\n }\n author {\n __typename\n avatarUrl\n }\n }\n ... on Reactable {\n viewerCanReact\n ... on Comment {\n author {\n __typename\n login\n }\n }\n }\n }\n }\n}" + } +} \ No newline at end of file diff --git a/Tests/StarWarsAPI/API.swift b/Tests/StarWarsAPI/API.swift index 6729318f81..b8ced72dcf 100644 --- a/Tests/StarWarsAPI/API.swift +++ b/Tests/StarWarsAPI/API.swift @@ -122,6 +122,8 @@ public final class HeroAndFriendsNamesQuery: GraphQLQuery { public let operationDefinition = "query HeroAndFriendsNames($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n friends {\n __typename\n name\n }\n }\n}" + public let operationIdentifier: String? = "fe3f21394eb861aa515c4d582e645469045793c9cbbeca4b5d4ce4d7dd617556" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -259,6 +261,8 @@ public final class HeroAndFriendsNamesWithIDsQuery: GraphQLQuery { public let operationDefinition = "query HeroAndFriendsNamesWithIDs($episode: Episode) {\n hero(episode: $episode) {\n __typename\n id\n name\n friends {\n __typename\n id\n name\n }\n }\n}" + public let operationIdentifier: String? = "8e4ca76c63660898cfd5a3845e3709027750b5f0151c7f9be65759b869c5486d" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -418,6 +422,8 @@ public final class HeroAndFriendsNamesWithIdForParentOnlyQuery: GraphQLQuery { public let operationDefinition = "query HeroAndFriendsNamesWithIDForParentOnly($episode: Episode) {\n hero(episode: $episode) {\n __typename\n id\n name\n friends {\n __typename\n name\n }\n }\n}" + public let operationIdentifier: String? = "f091468a629f3b757c03a1b7710c6ede8b5c8f10df7ba3238f2bbcd71c56f90f" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -566,6 +572,8 @@ public final class HeroAndFriendsNamesWithFragmentQuery: GraphQLQuery { public let operationDefinition = "query HeroAndFriendsNamesWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n ...FriendsNames\n }\n}" + public let operationIdentifier: String? = "1d3ad903dad146ff9d7aa09813fc01becd017489bfc1af8ffd178498730a5a26" + public var queryDocument: String { return operationDefinition.appending(FriendsNames.fragmentDefinition) } public var episode: Episode? @@ -732,6 +740,8 @@ public final class HeroAndFriendsNamesWithFragmentTwiceQuery: GraphQLQuery { public let operationDefinition = "query HeroAndFriendsNamesWithFragmentTwice($episode: Episode) {\n hero(episode: $episode) {\n __typename\n friends {\n __typename\n ...CharacterName\n }\n ... on Droid {\n friends {\n __typename\n ...CharacterName\n }\n }\n }\n}" + public let operationIdentifier: String? = "e02ef22e116ad1ca35f0298ed3badb60eeb986203f0088575a5f137768c322fc" + public var queryDocument: String { return operationDefinition.appending(CharacterName.fragmentDefinition) } public var episode: Episode? @@ -1012,6 +1022,8 @@ public final class HeroFriendsOfFriendsNamesQuery: GraphQLQuery { public let operationDefinition = "query HeroFriendsOfFriendsNames($episode: Episode) {\n hero(episode: $episode) {\n __typename\n friends {\n __typename\n id\n friends {\n __typename\n name\n }\n }\n }\n}" + public let operationIdentifier: String? = "37cd5626048e7243716ffda9e56503939dd189772124a1c21b0e0b87e69aae01" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -1191,6 +1203,8 @@ public final class HeroNameAndAppearsInWithFragmentQuery: GraphQLQuery { public let operationDefinition = "query HeroNameAndAppearsInWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...CharacterNameAndAppearsIn\n }\n}" + public let operationIdentifier: String? = "0664fed3eb4f9fbdb44e8691d9e8fd11f2b3c097ba11327592054f602bd3ba1a" + public var queryDocument: String { return operationDefinition.appending(CharacterNameAndAppearsIn.fragmentDefinition) } public var episode: Episode? @@ -1315,6 +1329,8 @@ public final class StarshipQuery: GraphQLQuery { public let operationDefinition = "query Starship {\n starship(id: 3000) {\n __typename\n name\n coordinates\n }\n}" + public let operationIdentifier: String? = "a3734516185da9919e3e66d74fe92b60d65292a1943dc54913f7332637dfdd2a" + public init() { } @@ -1398,6 +1414,8 @@ public final class HeroAppearsInQuery: GraphQLQuery { public let operationDefinition = "query HeroAppearsIn {\n hero {\n __typename\n appearsIn\n }\n}" + public let operationIdentifier: String? = "22d772c0fc813281705e8f0a55fc70e71eeff6e98f3f9ef96cf67fb896914522" + public init() { } @@ -1475,6 +1493,8 @@ public final class HeroAppearsInWithFragmentQuery: GraphQLQuery { public let operationDefinition = "query HeroAppearsInWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...CharacterAppearsIn\n }\n}" + public let operationIdentifier: String? = "1756158bd7736d58db45a48d74a724fa1b6fdac735376df8afac8318ba5431fb" + public var queryDocument: String { return operationDefinition.appending(CharacterAppearsIn.fragmentDefinition) } public var episode: Episode? @@ -1588,6 +1608,8 @@ public final class HeroDetailsQuery: GraphQLQuery { public let operationDefinition = "query HeroDetails($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n ... on Human {\n height\n }\n ... on Droid {\n primaryFunction\n }\n }\n}" + public let operationIdentifier: String? = "2b67111fd3a1c6b2ac7d1ef7764e5cefa41d3f4218e1d60cb67c22feafbd43ec" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -1797,6 +1819,8 @@ public final class HeroDetailsWithFragmentQuery: GraphQLQuery { public let operationDefinition = "query HeroDetailsWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...HeroDetails\n }\n}" + public let operationIdentifier: String? = "d20fa2f460058b8eec3d227f2f6088a708cf35dfa2b5ebf1414e34f9674ecfce" + public var queryDocument: String { return operationDefinition.appending(HeroDetails.fragmentDefinition) } public var episode: Episode? @@ -2093,6 +2117,8 @@ public final class DroidDetailsWithFragmentQuery: GraphQLQuery { public let operationDefinition = "query DroidDetailsWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...DroidDetails\n }\n}" + public let operationIdentifier: String? = "7277e97563e911ac8f5c91d401028d218aae41f38df014d7fa0b037bb2a2e739" + public var queryDocument: String { return operationDefinition.appending(DroidDetails.fragmentDefinition) } public var episode: Episode? @@ -2288,6 +2314,8 @@ public final class SameHeroTwiceQuery: GraphQLQuery { public let operationDefinition = "query SameHeroTwice {\n hero {\n __typename\n name\n }\n r2: hero {\n __typename\n appearsIn\n }\n}" + public let operationIdentifier: String? = "2a8ad85a703add7d64622aaf6be76b58a1134caf28e4ff6b34dd00ba89541364" + public init() { } @@ -2417,6 +2445,8 @@ public final class TwoHeroesQuery: GraphQLQuery { public let operationDefinition = "query TwoHeroes {\n r2: hero {\n __typename\n name\n }\n luke: hero(episode: EMPIRE) {\n __typename\n name\n }\n}" + public let operationIdentifier: String? = "b868fa9c48f19b8151c08c09f46831e3b9cd09f5c617d328647de785244b52bb" + public init() { } @@ -2546,6 +2576,8 @@ public final class HeroNameConditionalExclusionQuery: GraphQLQuery { public let operationDefinition = "query HeroNameConditionalExclusion($skipName: Boolean!) {\n hero {\n __typename\n name @skip(if: $skipName)\n }\n}" + public let operationIdentifier: String? = "3dd42259adf2d0598e89e0279bee2c128a7913f02b1da6aa43f3b5def6a8a1f8" + public var skipName: Bool public init(skipName: Bool) { @@ -2632,6 +2664,8 @@ public final class HeroNameConditionalInclusionQuery: GraphQLQuery { public let operationDefinition = "query HeroNameConditionalInclusion($includeName: Boolean!) {\n hero {\n __typename\n name @include(if: $includeName)\n }\n}" + public let operationIdentifier: String? = "338081aea3acc83d04af0741ecf0da1ec2ee8e6468a88383476b681015905ef8" + public var includeName: Bool public init(includeName: Bool) { @@ -2718,6 +2752,8 @@ public final class HeroNameConditionalBothQuery: GraphQLQuery { public let operationDefinition = "query HeroNameConditionalBoth($skipName: Boolean!, $includeName: Boolean!) {\n hero {\n __typename\n name @skip(if: $skipName) @include(if: $includeName)\n }\n}" + public let operationIdentifier: String? = "66f4dc124b6374b1912b22a2a208e34a4b1997349402a372b95bcfafc7884064" + public var skipName: Bool public var includeName: Bool @@ -2808,6 +2844,8 @@ public final class HeroNameConditionalBothSeparateQuery: GraphQLQuery { public let operationDefinition = "query HeroNameConditionalBothSeparate($skipName: Boolean!, $includeName: Boolean!) {\n hero {\n __typename\n name @skip(if: $skipName)\n name @include(if: $includeName)\n }\n}" + public let operationIdentifier: String? = "d0f9e9205cdc09320035662f528a177654d3275b0bf94cf0e259a65fde33e7e5" + public var skipName: Bool public var includeName: Bool @@ -2899,6 +2937,8 @@ public final class HeroDetailsInlineConditionalInclusionQuery: GraphQLQuery { public let operationDefinition = "query HeroDetailsInlineConditionalInclusion($includeDetails: Boolean!) {\n hero {\n __typename\n ... @include(if: $includeDetails) {\n name\n appearsIn\n }\n }\n}" + public let operationIdentifier: String? = "fcd9d7acb4e7c97e3ae5ad3cbf4e83556626149de589f0c2fce2f8ede31b0d90" + public var includeDetails: Bool public init(includeDetails: Bool) { @@ -2996,6 +3036,8 @@ public final class HeroDetailsFragmentConditionalInclusionQuery: GraphQLQuery { public let operationDefinition = "query HeroDetailsFragmentConditionalInclusion($includeDetails: Boolean!) {\n hero {\n __typename\n ...HeroDetails @include(if: $includeDetails)\n }\n}" + public let operationIdentifier: String? = "b31aec7d977249e185922e4cc90318fd2c7197631470904bf937b0626de54b4f" + public var queryDocument: String { return operationDefinition.appending(HeroDetails.fragmentDefinition) } public var includeDetails: Bool @@ -3306,6 +3348,8 @@ public final class HeroNameTypeSpecificConditionalInclusionQuery: GraphQLQuery { public let operationDefinition = "query HeroNameTypeSpecificConditionalInclusion($episode: Episode, $includeName: Boolean!) {\n hero(episode: $episode) {\n __typename\n name @include(if: $includeName)\n ... on Droid {\n name\n }\n }\n}" + public let operationIdentifier: String? = "4d465fbc6e3731d011025048502f16278307d73300ea9329a709d7e2b6815e40" + public var episode: Episode? public var includeName: Bool @@ -3451,6 +3495,8 @@ public final class HeroFriendsDetailsConditionalInclusionQuery: GraphQLQuery { public let operationDefinition = "query HeroFriendsDetailsConditionalInclusion($includeFriendsDetails: Boolean!) {\n hero {\n __typename\n friends @include(if: $includeFriendsDetails) {\n __typename\n name\n ... on Droid {\n primaryFunction\n }\n }\n }\n}" + public let operationIdentifier: String? = "9bdfeee789c1d22123402a9c3e3edefeb66799b3436289751be8f47905e3babd" + public var includeFriendsDetails: Bool public init(includeFriendsDetails: Bool) { @@ -3644,6 +3690,8 @@ public final class HeroFriendsDetailsUnconditionalAndConditionalInclusionQuery: public let operationDefinition = "query HeroFriendsDetailsUnconditionalAndConditionalInclusion($includeFriendsDetails: Boolean!) {\n hero {\n __typename\n friends {\n __typename\n name\n }\n friends @include(if: $includeFriendsDetails) {\n __typename\n name\n ... on Droid {\n primaryFunction\n }\n }\n }\n}" + public let operationIdentifier: String? = "501fcb710e5ffeeab2c65b7935fbded394ffea92e7b5dd904d05d5deab6f39c6" + public var includeFriendsDetails: Bool public init(includeFriendsDetails: Bool) { @@ -3850,6 +3898,8 @@ public final class HumanQuery: GraphQLQuery { public let operationDefinition = "query Human($id: ID!) {\n human(id: $id) {\n __typename\n name\n mass\n }\n}" + public let operationIdentifier: String? = "b37eb69b82fd52358321e49453769750983be1c286744dbf415735d7bcf12f1e" + public var id: GraphQLID public init(id: GraphQLID) { @@ -3941,6 +3991,8 @@ public final class HeroNameQuery: GraphQLQuery { public let operationDefinition = "query HeroName($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n }\n}" + public let operationIdentifier: String? = "f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -4025,6 +4077,8 @@ public final class HeroNameWithIdQuery: GraphQLQuery { public let operationDefinition = "query HeroNameWithID($episode: Episode) {\n hero(episode: $episode) {\n __typename\n id\n name\n }\n}" + public let operationIdentifier: String? = "83c03f612c46fca72f6cb902df267c57bffc9209bc44dd87d2524fb2b34f6f18" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -4120,6 +4174,8 @@ public final class HeroNameWithFragmentQuery: GraphQLQuery { public let operationDefinition = "query HeroNameWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...CharacterName\n }\n}" + public let operationIdentifier: String? = "b952f0054915a32ec524ac0dde0244bcda246649debe149f9e32e303e21c8266" + public var queryDocument: String { return operationDefinition.appending(CharacterName.fragmentDefinition) } public var episode: Episode? @@ -4233,6 +4289,8 @@ public final class HeroNameWithFragmentAndIdQuery: GraphQLQuery { public let operationDefinition = "query HeroNameWithFragmentAndID($episode: Episode) {\n hero(episode: $episode) {\n __typename\n id\n ...CharacterName\n }\n}" + public let operationIdentifier: String? = "a87a0694c09d1ed245e9a80f245d96a5f57b20a4aa936ee9ab09b2a43620db02" + public var queryDocument: String { return operationDefinition.appending(CharacterName.fragmentDefinition) } public var episode: Episode? @@ -4357,6 +4415,8 @@ public final class HeroTypeDependentAliasedFieldQuery: GraphQLQuery { public let operationDefinition = "query HeroTypeDependentAliasedField($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ... on Human {\n property: homePlanet\n }\n ... on Droid {\n property: primaryFunction\n }\n }\n}" + public let operationIdentifier: String? = "b5838c22bac1c5626023dac4412ca9b86bebfe16608991fb632a37c44e12811e" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -4533,6 +4593,8 @@ public final class ReviewAddedSubscription: GraphQLSubscription { public let operationDefinition = "subscription ReviewAdded($episode: Episode) {\n reviewAdded(episode: $episode) {\n __typename\n episode\n stars\n commentary\n }\n}" + public let operationIdentifier: String? = "38644c5e7cf4fd506b91d2e7010cabf84e63dfcd33cf1deb443b4b32b55e2cbe" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -4635,6 +4697,8 @@ public final class HeroParentTypeDependentFieldQuery: GraphQLQuery { public let operationDefinition = "query HeroParentTypeDependentField($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n ... on Human {\n friends {\n __typename\n name\n ... on Human {\n height(unit: FOOT)\n }\n }\n }\n ... on Droid {\n friends {\n __typename\n name\n ... on Human {\n height(unit: METER)\n }\n }\n }\n }\n}" + public let operationIdentifier: String? = "561e22ac4da5209f254779b70e01557fb2fc57916b9914088429ec809e166cad" + public var episode: Episode? public init(episode: Episode? = nil) { @@ -5058,6 +5122,8 @@ public final class CreateReviewForEpisodeMutation: GraphQLMutation { public let operationDefinition = "mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {\n createReview(episode: $episode, review: $review) {\n __typename\n stars\n commentary\n }\n}" + public let operationIdentifier: String? = "9bbf5b4074d0635fb19d17c621b7b04ebfb1920d468a94266819e149841e7d5d" + public var episode: Episode public var review: ReviewInput @@ -5151,6 +5217,8 @@ public final class CreateAwesomeReviewMutation: GraphQLMutation { public let operationDefinition = "mutation CreateAwesomeReview {\n createReview(episode: JEDI, review: {stars: 10, commentary: \"This is awesome!\"}) {\n __typename\n stars\n commentary\n }\n}" + public let operationIdentifier: String? = "4a1250de93ebcb5cad5870acf15001112bf27bb963e8709555b5ff67a1405374" + public init() { } diff --git a/Tests/StarWarsAPI/operationIdsPath.json b/Tests/StarWarsAPI/operationIdsPath.json new file mode 100644 index 0000000000..c1cea076d4 --- /dev/null +++ b/Tests/StarWarsAPI/operationIdsPath.json @@ -0,0 +1,138 @@ +{ + "fe3f21394eb861aa515c4d582e645469045793c9cbbeca4b5d4ce4d7dd617556": { + "name": "HeroAndFriendsNames", + "source": "query HeroAndFriendsNames($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n friends {\n __typename\n name\n }\n }\n}" + }, + "8e4ca76c63660898cfd5a3845e3709027750b5f0151c7f9be65759b869c5486d": { + "name": "HeroAndFriendsNamesWithIDs", + "source": "query HeroAndFriendsNamesWithIDs($episode: Episode) {\n hero(episode: $episode) {\n __typename\n id\n name\n friends {\n __typename\n id\n name\n }\n }\n}" + }, + "f091468a629f3b757c03a1b7710c6ede8b5c8f10df7ba3238f2bbcd71c56f90f": { + "name": "HeroAndFriendsNamesWithIDForParentOnly", + "source": "query HeroAndFriendsNamesWithIDForParentOnly($episode: Episode) {\n hero(episode: $episode) {\n __typename\n id\n name\n friends {\n __typename\n name\n }\n }\n}" + }, + "1d3ad903dad146ff9d7aa09813fc01becd017489bfc1af8ffd178498730a5a26": { + "name": "HeroAndFriendsNamesWithFragment", + "source": "query HeroAndFriendsNamesWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n ...FriendsNames\n }\n}" + }, + "e02ef22e116ad1ca35f0298ed3badb60eeb986203f0088575a5f137768c322fc": { + "name": "HeroAndFriendsNamesWithFragmentTwice", + "source": "query HeroAndFriendsNamesWithFragmentTwice($episode: Episode) {\n hero(episode: $episode) {\n __typename\n friends {\n __typename\n ...CharacterName\n }\n ... on Droid {\n friends {\n __typename\n ...CharacterName\n }\n }\n }\n}" + }, + "37cd5626048e7243716ffda9e56503939dd189772124a1c21b0e0b87e69aae01": { + "name": "HeroFriendsOfFriendsNames", + "source": "query HeroFriendsOfFriendsNames($episode: Episode) {\n hero(episode: $episode) {\n __typename\n friends {\n __typename\n id\n friends {\n __typename\n name\n }\n }\n }\n}" + }, + "0664fed3eb4f9fbdb44e8691d9e8fd11f2b3c097ba11327592054f602bd3ba1a": { + "name": "HeroNameAndAppearsInWithFragment", + "source": "query HeroNameAndAppearsInWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...CharacterNameAndAppearsIn\n }\n}" + }, + "a3734516185da9919e3e66d74fe92b60d65292a1943dc54913f7332637dfdd2a": { + "name": "Starship", + "source": "query Starship {\n starship(id: 3000) {\n __typename\n name\n coordinates\n }\n}" + }, + "22d772c0fc813281705e8f0a55fc70e71eeff6e98f3f9ef96cf67fb896914522": { + "name": "HeroAppearsIn", + "source": "query HeroAppearsIn {\n hero {\n __typename\n appearsIn\n }\n}" + }, + "1756158bd7736d58db45a48d74a724fa1b6fdac735376df8afac8318ba5431fb": { + "name": "HeroAppearsInWithFragment", + "source": "query HeroAppearsInWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...CharacterAppearsIn\n }\n}" + }, + "2b67111fd3a1c6b2ac7d1ef7764e5cefa41d3f4218e1d60cb67c22feafbd43ec": { + "name": "HeroDetails", + "source": "query HeroDetails($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n ... on Human {\n height\n }\n ... on Droid {\n primaryFunction\n }\n }\n}" + }, + "d20fa2f460058b8eec3d227f2f6088a708cf35dfa2b5ebf1414e34f9674ecfce": { + "name": "HeroDetailsWithFragment", + "source": "query HeroDetailsWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...HeroDetails\n }\n}" + }, + "7277e97563e911ac8f5c91d401028d218aae41f38df014d7fa0b037bb2a2e739": { + "name": "DroidDetailsWithFragment", + "source": "query DroidDetailsWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...DroidDetails\n }\n}" + }, + "2a8ad85a703add7d64622aaf6be76b58a1134caf28e4ff6b34dd00ba89541364": { + "name": "SameHeroTwice", + "source": "query SameHeroTwice {\n hero {\n __typename\n name\n }\n r2: hero {\n __typename\n appearsIn\n }\n}" + }, + "b868fa9c48f19b8151c08c09f46831e3b9cd09f5c617d328647de785244b52bb": { + "name": "TwoHeroes", + "source": "query TwoHeroes {\n r2: hero {\n __typename\n name\n }\n luke: hero(episode: EMPIRE) {\n __typename\n name\n }\n}" + }, + "3dd42259adf2d0598e89e0279bee2c128a7913f02b1da6aa43f3b5def6a8a1f8": { + "name": "HeroNameConditionalExclusion", + "source": "query HeroNameConditionalExclusion($skipName: Boolean!) {\n hero {\n __typename\n name @skip(if: $skipName)\n }\n}" + }, + "338081aea3acc83d04af0741ecf0da1ec2ee8e6468a88383476b681015905ef8": { + "name": "HeroNameConditionalInclusion", + "source": "query HeroNameConditionalInclusion($includeName: Boolean!) {\n hero {\n __typename\n name @include(if: $includeName)\n }\n}" + }, + "66f4dc124b6374b1912b22a2a208e34a4b1997349402a372b95bcfafc7884064": { + "name": "HeroNameConditionalBoth", + "source": "query HeroNameConditionalBoth($skipName: Boolean!, $includeName: Boolean!) {\n hero {\n __typename\n name @skip(if: $skipName) @include(if: $includeName)\n }\n}" + }, + "d0f9e9205cdc09320035662f528a177654d3275b0bf94cf0e259a65fde33e7e5": { + "name": "HeroNameConditionalBothSeparate", + "source": "query HeroNameConditionalBothSeparate($skipName: Boolean!, $includeName: Boolean!) {\n hero {\n __typename\n name @skip(if: $skipName)\n name @include(if: $includeName)\n }\n}" + }, + "fcd9d7acb4e7c97e3ae5ad3cbf4e83556626149de589f0c2fce2f8ede31b0d90": { + "name": "HeroDetailsInlineConditionalInclusion", + "source": "query HeroDetailsInlineConditionalInclusion($includeDetails: Boolean!) {\n hero {\n __typename\n ... @include(if: $includeDetails) {\n name\n appearsIn\n }\n }\n}" + }, + "b31aec7d977249e185922e4cc90318fd2c7197631470904bf937b0626de54b4f": { + "name": "HeroDetailsFragmentConditionalInclusion", + "source": "query HeroDetailsFragmentConditionalInclusion($includeDetails: Boolean!) {\n hero {\n __typename\n ...HeroDetails @include(if: $includeDetails)\n }\n}" + }, + "4d465fbc6e3731d011025048502f16278307d73300ea9329a709d7e2b6815e40": { + "name": "HeroNameTypeSpecificConditionalInclusion", + "source": "query HeroNameTypeSpecificConditionalInclusion($episode: Episode, $includeName: Boolean!) {\n hero(episode: $episode) {\n __typename\n name @include(if: $includeName)\n ... on Droid {\n name\n }\n }\n}" + }, + "9bdfeee789c1d22123402a9c3e3edefeb66799b3436289751be8f47905e3babd": { + "name": "HeroFriendsDetailsConditionalInclusion", + "source": "query HeroFriendsDetailsConditionalInclusion($includeFriendsDetails: Boolean!) {\n hero {\n __typename\n friends @include(if: $includeFriendsDetails) {\n __typename\n name\n ... on Droid {\n primaryFunction\n }\n }\n }\n}" + }, + "501fcb710e5ffeeab2c65b7935fbded394ffea92e7b5dd904d05d5deab6f39c6": { + "name": "HeroFriendsDetailsUnconditionalAndConditionalInclusion", + "source": "query HeroFriendsDetailsUnconditionalAndConditionalInclusion($includeFriendsDetails: Boolean!) {\n hero {\n __typename\n friends {\n __typename\n name\n }\n friends @include(if: $includeFriendsDetails) {\n __typename\n name\n ... on Droid {\n primaryFunction\n }\n }\n }\n}" + }, + "b37eb69b82fd52358321e49453769750983be1c286744dbf415735d7bcf12f1e": { + "name": "Human", + "source": "query Human($id: ID!) {\n human(id: $id) {\n __typename\n name\n mass\n }\n}" + }, + "f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671": { + "name": "HeroName", + "source": "query HeroName($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n }\n}" + }, + "83c03f612c46fca72f6cb902df267c57bffc9209bc44dd87d2524fb2b34f6f18": { + "name": "HeroNameWithID", + "source": "query HeroNameWithID($episode: Episode) {\n hero(episode: $episode) {\n __typename\n id\n name\n }\n}" + }, + "b952f0054915a32ec524ac0dde0244bcda246649debe149f9e32e303e21c8266": { + "name": "HeroNameWithFragment", + "source": "query HeroNameWithFragment($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ...CharacterName\n }\n}" + }, + "a87a0694c09d1ed245e9a80f245d96a5f57b20a4aa936ee9ab09b2a43620db02": { + "name": "HeroNameWithFragmentAndID", + "source": "query HeroNameWithFragmentAndID($episode: Episode) {\n hero(episode: $episode) {\n __typename\n id\n ...CharacterName\n }\n}" + }, + "b5838c22bac1c5626023dac4412ca9b86bebfe16608991fb632a37c44e12811e": { + "name": "HeroTypeDependentAliasedField", + "source": "query HeroTypeDependentAliasedField($episode: Episode) {\n hero(episode: $episode) {\n __typename\n ... on Human {\n property: homePlanet\n }\n ... on Droid {\n property: primaryFunction\n }\n }\n}" + }, + "38644c5e7cf4fd506b91d2e7010cabf84e63dfcd33cf1deb443b4b32b55e2cbe": { + "name": "ReviewAdded", + "source": "subscription ReviewAdded($episode: Episode) {\n reviewAdded(episode: $episode) {\n __typename\n episode\n stars\n commentary\n }\n}" + }, + "561e22ac4da5209f254779b70e01557fb2fc57916b9914088429ec809e166cad": { + "name": "HeroParentTypeDependentField", + "source": "query HeroParentTypeDependentField($episode: Episode) {\n hero(episode: $episode) {\n __typename\n name\n ... on Human {\n friends {\n __typename\n name\n ... on Human {\n height(unit: FOOT)\n }\n }\n }\n ... on Droid {\n friends {\n __typename\n name\n ... on Human {\n height(unit: METER)\n }\n }\n }\n }\n}" + }, + "9bbf5b4074d0635fb19d17c621b7b04ebfb1920d468a94266819e149841e7d5d": { + "name": "CreateReviewForEpisode", + "source": "mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {\n createReview(episode: $episode, review: $review) {\n __typename\n stars\n commentary\n }\n}" + }, + "4a1250de93ebcb5cad5870acf15001112bf27bb963e8709555b5ff67a1405374": { + "name": "CreateAwesomeReview", + "source": "mutation CreateAwesomeReview {\n createReview(episode: JEDI, review: {stars: 10, commentary: \"This is awesome!\"}) {\n __typename\n stars\n commentary\n }\n}" + } +} \ No newline at end of file