Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create FetchOptions to allow GET Requests like in Web #572

Merged
merged 12 commits into from
Jun 26, 2019
4 changes: 4 additions & 0 deletions Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */; };
5AC6CA4322AAF7B200B7C94D /* FetchHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC6CA4222AAF7B200B7C94D /* FetchHTTPMethod.swift */; };
9F0CA4451EE7F9E90032DD39 /* ApolloTestSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; };
9F0CA4461EE7F9E90032DD39 /* ApolloTestSupport.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9F10A51E1EC1BA0F0045E62B /* MockNetworkTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10A51D1EC1BA0F0045E62B /* MockNetworkTransport.swift */; };
Expand Down Expand Up @@ -230,6 +231,7 @@

/* Begin PBXFileReference section */
54DDB0911EA045870009DD99 /* InMemoryNormalizedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryNormalizedCache.swift; sourceTree = "<group>"; };
5AC6CA4222AAF7B200B7C94D /* FetchHTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchHTTPMethod.swift; sourceTree = "<group>"; };
90690D05224333DA00FC2E54 /* Apollo-Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Project-Debug.xcconfig"; sourceTree = "<group>"; };
90690D06224333DA00FC2E54 /* Apollo-Target-Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-Framework.xcconfig"; sourceTree = "<group>"; };
90690D07224333DA00FC2E54 /* Apollo-Project-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Project-Release.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -535,6 +537,7 @@
isa = PBXGroup;
children = (
9FC750621D2A59F600458D91 /* ApolloClient.swift */,
5AC6CA4222AAF7B200B7C94D /* FetchHTTPMethod.swift */,
9FC750601D2A59C300458D91 /* GraphQLOperation.swift */,
9FC9A9BE1E2C27FB0023C4D5 /* GraphQLResult.swift */,
9FC9A9D21E2FD48B0023C4D5 /* GraphQLError.swift */,
Expand Down Expand Up @@ -1083,6 +1086,7 @@
9FA6F3681E65DF4700BF8D73 /* GraphQLResultAccumulator.swift in Sources */,
9FF90A651DDDEB100034C3B6 /* GraphQLExecutor.swift in Sources */,
9FC750611D2A59C300458D91 /* GraphQLOperation.swift in Sources */,
5AC6CA4322AAF7B200B7C94D /* FetchHTTPMethod.swift in Sources */,
9FE941D01E62C771007CDD89 /* Promise.swift in Sources */,
9FC750631D2A59F600458D91 /* ApolloClient.swift in Sources */,
9F86B6901E65533D00B885FF /* GraphQLResponseGenerator.swift in Sources */,
Expand Down
40 changes: 23 additions & 17 deletions Sources/Apollo/ApolloClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,22 @@ public class ApolloClient {
///
/// - Parameters:
/// - query: The query to fetch.
/// - fetchHTTPMethod: The HTTP Method to be used.
/// - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache.
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
/// - resultHandler: An optional closure that is called when query results are available or when an error occurs.
/// - Returns: An object that can be used to cancel an in progress fetch.
@discardableResult public func fetch<Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Query>? = nil) -> Cancellable {
return _fetch(query: query, cachePolicy: cachePolicy, queue: queue, resultHandler: resultHandler)
@discardableResult public func fetch<Query: GraphQLQuery>(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Query>? = nil) -> Cancellable {
return _fetch(query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, queue: queue, resultHandler: resultHandler)
}

func _fetch<Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) -> Cancellable {
func _fetch<Query: GraphQLQuery>(query: Query, fetchHTTPMethod: FetchHTTPMethod, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) -> Cancellable {
// If we don't have to go through the cache, there is no need to create an operation
// and we can return a network task directly
if cachePolicy == .fetchIgnoringCacheData {
return send(operation: query, context: context, handlerQueue: queue, resultHandler: resultHandler)
return send(operation: query, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: queue, resultHandler: resultHandler)
} else {
let operation = FetchQueryOperation(client: self, query: query, cachePolicy: cachePolicy, context: context, handlerQueue: queue, resultHandler: resultHandler)
let operation = FetchQueryOperation(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, context: context, handlerQueue: queue, resultHandler: resultHandler)
operationQueue.addOperation(operation)
return operation
}
Expand All @@ -100,12 +101,13 @@ public class ApolloClient {
///
/// - Parameters:
/// - query: The query to fetch.
/// - fetchHTTPMethod: The HTTP Method to be used.
/// - cachePolicy: A cache policy that specifies when results should be fetched from the server or from the local cache.
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
/// - resultHandler: An optional closure that is called when query results are available or when an error occurs.
/// - Returns: A query watcher object that can be used to control the watching behavior.
public func watch<Query: GraphQLQuery>(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Query>) -> GraphQLQueryWatcher<Query> {
let watcher = GraphQLQueryWatcher(client: self, query: query, handlerQueue: queue, resultHandler: resultHandler)
public func watch<Query: GraphQLQuery>(query: Query, fetchHTTPMethod: FetchHTTPMethod = .POST, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Query>) -> GraphQLQueryWatcher<Query> {
let watcher = GraphQLQueryWatcher(client: self, query: query, fetchHTTPMethod: fetchHTTPMethod, handlerQueue: queue, resultHandler: resultHandler)
watcher.fetch(cachePolicy: cachePolicy)
return watcher
}
Expand All @@ -114,30 +116,32 @@ public class ApolloClient {
///
/// - Parameters:
/// - mutation: The mutation to perform.
/// - fetchHTTPMethod: The HTTP Method to be used.
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
/// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs.
/// - Returns: An object that can be used to cancel an in progress mutation.
@discardableResult public func perform<Mutation: GraphQLMutation>(mutation: Mutation, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Mutation>? = nil) -> Cancellable {
return _perform(mutation: mutation, queue: queue, resultHandler: resultHandler)
@discardableResult public func perform<Mutation: GraphQLMutation>(mutation: Mutation, fetchHTTPMethod: FetchHTTPMethod = .POST, queue: DispatchQueue = DispatchQueue.main, resultHandler: OperationResultHandler<Mutation>? = nil) -> Cancellable {
return _perform(mutation: mutation, fetchHTTPMethod: fetchHTTPMethod, queue: queue, resultHandler: resultHandler)
}

func _perform<Mutation: GraphQLMutation>(mutation: Mutation, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Mutation>?) -> Cancellable {
return send(operation: mutation, context: context, handlerQueue: queue, resultHandler: resultHandler)
func _perform<Mutation: GraphQLMutation>(mutation: Mutation, fetchHTTPMethod: FetchHTTPMethod, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue, resultHandler: OperationResultHandler<Mutation>?) -> Cancellable {
return send(operation: mutation, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: queue, resultHandler: resultHandler)
}

/// Subscribe to a subscription
///
/// - Parameters:
/// - subscription: The subscription to subscribe to.
/// - fetchHTTPMethod: The HTTP Method to be used.
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
/// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs.
/// - Returns: An object that can be used to cancel an in progress subscription.
@discardableResult public func subscribe<Subscription: GraphQLSubscription>(subscription: Subscription, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Subscription>) -> Cancellable {
return send(operation: subscription, context: nil, handlerQueue: queue, resultHandler: resultHandler)
@discardableResult public func subscribe<Subscription: GraphQLSubscription>(subscription: Subscription, fetchHTTPMethod: FetchHTTPMethod = .POST, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping OperationResultHandler<Subscription>) -> Cancellable {
return send(operation: subscription, fetchHTTPMethod: fetchHTTPMethod, context: nil, handlerQueue: queue, resultHandler: resultHandler)
}


fileprivate func send<Operation: GraphQLOperation>(operation: Operation, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Operation>?) -> Cancellable {
fileprivate func send<Operation: GraphQLOperation>(operation: Operation, fetchHTTPMethod: FetchHTTPMethod, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Operation>?) -> Cancellable {
func notifyResultHandler(result: GraphQLResult<Operation.Data>?, error: Error?) {
guard let resultHandler = resultHandler else { return }

Expand All @@ -146,7 +150,7 @@ public class ApolloClient {
}
}

return networkTransport.send(operation: operation) { (response, error) in
return networkTransport.send(operation: operation, fetchHTTPMethod: fetchHTTPMethod) { (response, error) in
guard let response = response else {
notifyResultHandler(result: nil, error: error)
return
Expand Down Expand Up @@ -175,16 +179,18 @@ public class ApolloClient {
private final class FetchQueryOperation<Query: GraphQLQuery>: AsynchronousOperation, Cancellable {
let client: ApolloClient
let query: Query
let fetchHTTPMethod: FetchHTTPMethod
let cachePolicy: CachePolicy
let context: UnsafeMutableRawPointer?
let handlerQueue: DispatchQueue
let resultHandler: OperationResultHandler<Query>?

private var networkTask: Cancellable?

init(client: ApolloClient, query: Query, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) {
init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, cachePolicy: CachePolicy, context: UnsafeMutableRawPointer?, handlerQueue: DispatchQueue, resultHandler: OperationResultHandler<Query>?) {
self.client = client
self.query = query
self.fetchHTTPMethod = fetchHTTPMethod
self.cachePolicy = cachePolicy
self.context = context
self.handlerQueue = handlerQueue
Expand Down Expand Up @@ -230,7 +236,7 @@ private final class FetchQueryOperation<Query: GraphQLQuery>: AsynchronousOperat
}

func fetchFromNetwork() {
networkTask = client.send(operation: query, context: context, handlerQueue: handlerQueue) { (result, error) in
networkTask = client.send(operation: query, fetchHTTPMethod: fetchHTTPMethod, context: context, handlerQueue: handlerQueue) { (result, error) in
self.notifyResultHandler(result: result, error: error)
self.state = .finished
return
Expand Down
6 changes: 6 additions & 0 deletions Sources/Apollo/FetchHTTPMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

public enum FetchHTTPMethod: String {
case GET
case POST
}
6 changes: 4 additions & 2 deletions Sources/Apollo/GraphQLQueryWatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Dispatch
public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, ApolloStoreSubscriber {
weak var client: ApolloClient?
let query: Query
let fetchHTTPMethod: FetchHTTPMethod
let handlerQueue: DispatchQueue
let resultHandler: OperationResultHandler<Query>

Expand All @@ -13,9 +14,10 @@ public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, Apollo

private var dependentKeys: Set<CacheKey>?

init(client: ApolloClient, query: Query, handlerQueue: DispatchQueue, resultHandler: @escaping OperationResultHandler<Query>) {
init(client: ApolloClient, query: Query, fetchHTTPMethod: FetchHTTPMethod, handlerQueue: DispatchQueue, resultHandler: @escaping OperationResultHandler<Query>) {
self.client = client
self.query = query
self.fetchHTTPMethod = fetchHTTPMethod
self.handlerQueue = handlerQueue
self.resultHandler = resultHandler

Expand All @@ -28,7 +30,7 @@ public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, Apollo
}

func fetch(cachePolicy: CachePolicy) {
fetching = client?._fetch(query: query, cachePolicy: cachePolicy, context: &context, queue: handlerQueue) { [weak self] (result, error) in
fetching = client?._fetch(query: query, fetchHTTPMethod: fetchHTTPMethod, cachePolicy: cachePolicy, context: &context, queue: handlerQueue) { [weak self] (result, error) in
guard let `self` = self else { return }

self.dependentKeys = result?.dependentKeys
Expand Down
100 changes: 91 additions & 9 deletions Sources/Apollo/HTTPNetworkTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@ public struct GraphQLHTTPResponseError: Error, LocalizedError {
}
}

public struct GraphQLHTTPRequestError: Error, LocalizedError {
designatednerd marked this conversation as resolved.
Show resolved Hide resolved
public enum ErrorKind {
designatednerd marked this conversation as resolved.
Show resolved Hide resolved
case serializedBodyMessageError
case serializedQueryParamsMessageError

var description: String {
switch self {
case .serializedBodyMessageError:
return "JSONSerialization error: Error while serializing request's body"
case .serializedQueryParamsMessageError:
return "QueryParams error: Error while serializing variables as query parameters."
}
}
}

public init(kind: ErrorKind) {
self.kind = kind
}

public let kind: ErrorKind

public var errorDescription: String? {
return "\(kind.description)"
}
}

/// 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
Expand All @@ -69,39 +95,53 @@ public class HTTPNetworkTransport: NetworkTransport {
///
/// - Parameters:
/// - operation: The operation to send.
/// - fetchHTTPMethod: The HTTP Method to be used in operation.
/// - completionHandler: A closure to call when a request completes.
/// - response: The response received from the server, or `nil` if an error occurred.
/// - 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: Operation, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
public func send<Operation>(operation: Operation, fetchHTTPMethod: FetchHTTPMethod, completionHandler: @escaping (_ response: GraphQLResponse<Operation>?, _ error: Error?) -> Void) -> Cancellable {
let body = requestBody(for: operation)
var request = URLRequest(url: url)
request.httpMethod = "POST"

switch fetchHTTPMethod {
case .GET:
if let urlForGet = mountUrlWithQueryParamsIfNeeded(body: body) {
request = URLRequest(url: urlForGet)
} else {
completionHandler(nil, GraphQLHTTPRequestError(kind: .serializedQueryParamsMessageError))
}
default:
do {
request.httpBody = try serializationFormat.serialize(value: body)
} catch {
completionHandler(nil, GraphQLHTTPRequestError(kind: .serializedBodyMessageError))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably return here and above to prevent the rest of this from executing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi!

This method send<Operation> should return a Cancellable. In this case, it should return a task after the request. If it fails, it will execute the request and returns another error.

As the task must resume ( task.resume() ), what do you think of instead of do/try, we use a try? to let the request executes and returns the correct error? Another option would be change the return type to be optional as Cancellable?. But I don't like that much this approach because it would change the whole architecture of apollo-client and also, the request would never be executed in this case. And I'm not sure that it would be a good idea for clients.

What do you think? Do you have any suggestion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll poke around - I think there's probably something else that conforms to Cancelable that we can return here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay! Thank you, once again!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now the only non private object/struct is an extension of URLSessionTask declared inside this class. The others are the classes GraphQLQueryWatcher and WebSocketTask which are not supposed to be used in here.

But I was thinking about it... A request can be executed without a request body or a query parameter. It's like a request to an endpoint without parameters. So, if this try fails, it won't have a request body, and so, when the task resume, it will just return an error and the reason for it.

But also, the only way for this to fail, right now, would be in the case that Operation fails to be constructed. Which means a bad format in query that would fail while compiling or maybe during the processing.

Maybe, that's why it was using try! before. I do not like to force a try, but changing it for try? wouldn't cause a crash. And because we are confident that GraphQLOperation only exists if it succeeded parsing those values in query, the worst case scenario would be a request to an endpoint without parameter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But once again, tell me what is your opinion, please! I want to help with it, but if you want to keep this change and find another way to solve it, please, be my guest to fix it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for this PR we're gonna leave it - I'll make a fix that addresses this in other areas as well in a separate PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thank you for the review. :) !!

}
}

request.httpMethod = fetchHTTPMethod.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

let body = requestBody(for: operation)
request.httpBody = try! serializationFormat.serialize(value: body)

let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil {
completionHandler(nil, error)
return
}

guard let httpResponse = response as? HTTPURLResponse else {
fatalError("Response should be an HTTPURLResponse")
}

if (!httpResponse.isSuccessful) {
completionHandler(nil, GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .errorResponse))
return
}

guard let data = data else {
completionHandler(nil, GraphQLHTTPResponseError(body: nil, response: httpResponse, kind: .invalidResponse))
return
}

do {
guard let body = try self.serializationFormat.deserialize(data: data) as? JSONObject else {
throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse)
Expand Down Expand Up @@ -129,4 +169,46 @@ public class HTTPNetworkTransport: NetworkTransport {
}
return ["query": operation.queryDocument, "variables": operation.variables]
}

private func mountUrlWithQueryParamsIfNeeded(body: GraphQLMap) -> URL? {
guard let query = body.jsonObject["query"], var queryParam = queryString(withItems: [URLQueryItem(name: "query", value: "\(query)")]) else {
return self.url
}
if areThereVariables(in: body) {
guard let serializedVariables = try? serializationFormat.serialize(value: body.jsonObject["variables"]) else {
return URL(string: "\(self.url.absoluteString)?\(queryParam)")
}
queryParam += getVariablesEncodedString(of: serializedVariables)
}
guard let urlForGet = URL(string: "\(self.url.absoluteString)?\(queryParam)") else {
return URL(string: "\(self.url.absoluteString)?\(queryParam)")
}
return urlForGet
}

private func areThereVariables(in map: GraphQLMap) -> Bool {
if let variables = map.jsonObject["variables"], "\(variables)" != "<null>" {
return true
}
return false
}

private func getVariablesEncodedString(of data: Data) -> String {
var dataString = String(data: data, encoding: String.Encoding.utf8) ?? ""
dataString = dataString.replacingOccurrences(of: ";", with: ",")
dataString = dataString.replacingOccurrences(of: "=", with: ":")
guard let variablesEncoded = queryString(withItems: [URLQueryItem(name: "variables", value: "\(dataString)")]) else { return "" }
return "&\(variablesEncoded)"
}

private func queryString(withItems items: [URLQueryItem], percentEncoded: Bool = true) -> String? {
let url = NSURLComponents()
url.queryItems = items
let queryString = percentEncoded ? url.percentEncodedQuery : url.query

if let queryString = queryString {
return "\(queryString)"
}
return nil
}
}
Loading