Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0830;
LastUpgradeCheck = 1020;
LastUpgradeCheck = 1030;
ORGANIZATIONNAME = "Apollo GraphQL";
TargetAttributes = {
9F8A95771EC0FC1200304A2D = {
Expand Down
2 changes: 1 addition & 1 deletion Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1030"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1030"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1030"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
LastUpgradeVersion = "1030"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
92 changes: 67 additions & 25 deletions Sources/Apollo/ApolloClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ public class ApolloClient {
private let queue: DispatchQueue
private let operationQueue: OperationQueue

public enum ApolloClientError: Error, LocalizedError {
case noUploadTransport

public var localizedDescription: String {
switch self {
case .noUploadTransport:
return "Attempting to upload using a transport which does not support uploads. This is a developer error."
}
}
}

/// Creates a client with the specified network transport and store.
///
/// - Parameters:
Expand Down Expand Up @@ -117,6 +128,30 @@ public class ApolloClient {
@discardableResult public func perform<Mutation: GraphQLMutation>(mutation: Mutation, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler<Mutation.Data>? = nil) -> Cancellable {
return send(operation: mutation, shouldPublishResultToStore: true, context: context, resultHandler: wrapResultHandler(resultHandler, queue: queue))
}

/// Uploads the given files with the given operation.
///
/// - Parameters:
/// - operation: The operation to send
/// - files: An array of `GraphQLFile` objects to send.
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
/// - completionHandler: The completion handler to execute when the request completes or errors
/// - Returns: An object that can be used to cancel an in progress request.
/// - Throws: If your `networkTransport` does nto also conform to `UploadingNetworkTransport`.
@discardableResult public func upload<Operation: GraphQLOperation>(operation: Operation, context: UnsafeMutableRawPointer? = nil, files: [GraphQLFile], queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler<Operation.Data>? = nil) -> Cancellable {
let wrappedHandler = wrapResultHandler(resultHandler, queue: queue)
guard let uploadingTransport = self.networkTransport as? UploadingNetworkTransport else {
assertionFailure("Trying to upload without an uploading transport. Please make sure your network transport conforms to `UploadingNetworkTransport`.")
wrappedHandler(.failure(ApolloClientError.noUploadTransport))
return EmptyCancellable()
}

return uploadingTransport.upload(operation: operation, files: files) { result in
self.handleOperationResult(shouldPublishResultToStore: true,
context: context, result,
resultHandler: wrappedHandler)
}
}

/// Subscribe to a subscription
///
Expand All @@ -132,33 +167,40 @@ public class ApolloClient {

fileprivate func send<Operation: GraphQLOperation>(operation: Operation, shouldPublishResultToStore: Bool, context: UnsafeMutableRawPointer?, resultHandler: @escaping GraphQLResultHandler<Operation.Data>) -> Cancellable {
return networkTransport.send(operation: operation) { result in
switch result {
case .failure(let error):
resultHandler(.failure(error))
case .success(let response):
// If there is no need to publish the result to the store, we can use a fast path.
if !shouldPublishResultToStore {
do {
let result = try response.parseResultFast()
resultHandler(.success(result))
} catch {
resultHandler(.failure(error))
}
return
self.handleOperationResult(shouldPublishResultToStore: shouldPublishResultToStore,
context: context,
result,
resultHandler: resultHandler)
}
}

private func handleOperationResult<Operation>(shouldPublishResultToStore: Bool, context: UnsafeMutableRawPointer?, _ result: Result<GraphQLResponse<Operation>, Error>, resultHandler: @escaping GraphQLResultHandler<Operation.Data>) {
switch result {
case .failure(let error):
resultHandler(.failure(error))
case .success(let response):
// If there is no need to publish the result to the store, we can use a fast path.
if !shouldPublishResultToStore {
do {
let result = try response.parseResultFast()
resultHandler(.success(result))
} catch {
resultHandler(.failure(error))
}

firstly {
try response.parseResult(cacheKeyForObject: self.cacheKeyForObject)
}.andThen { (result, records) in
if let records = records {
self.store.publish(records: records, context: context).catch { error in
preconditionFailure(String(describing: error))
}
return
}

firstly {
try response.parseResult(cacheKeyForObject: self.cacheKeyForObject)
}.andThen { (result, records) in
if let records = records {
self.store.publish(records: records, context: context).catch { error in
preconditionFailure(String(describing: error))
}
resultHandler(.success(result))
}.catch { error in
resultHandler(.failure(error))
}
}
resultHandler(.success(result))
}.catch { error in
resultHandler(.failure(error))
}
}
}
Expand Down
27 changes: 13 additions & 14 deletions Sources/Apollo/HTTPNetworkTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,7 @@ public class HTTPNetworkTransport {
self.useGETForQueries = useGETForQueries
self.delegate = delegate
}

/// Uploads the given files with the given operation.
///
/// - Parameters:
/// - operation: The operation to send
/// - files: An array of `GraphQLFile` objects to send.
/// - completionHandler: The completion handler to execute when the request completes or errors
/// - Returns: An object that can be used to cancel an in progress request.
public func upload<Operation>(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable {
return send(operation: operation, files: files, completionHandler: completionHandler)
}


private func send<Operation>(operation: Operation, files: [GraphQLFile]?, completionHandler: @escaping (_ results: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable {
let request: URLRequest
do {
Expand Down Expand Up @@ -227,6 +216,9 @@ public class HTTPNetworkTransport {
let body = RequestCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers)
var request = URLRequest(url: self.url)

// We default to json, but this can be changed below if needed.
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

if self.useGETForQueries && operation.operationType == .query {
let transformer = GraphQLGETTransformer(body: body, url: self.url)
if let urlForGet = transformer.createGetURL() {
Expand Down Expand Up @@ -262,8 +254,6 @@ public class HTTPNetworkTransport {
request.setValue(operationID, forHTTPHeaderField: "X-APOLLO-OPERATION-ID")
}

request.setValue("application/json", forHTTPHeaderField: "Content-Type")

// If there's a delegate, do a pre-flight check and allow modifications to the request.
if
let delegate = self.delegate,
Expand All @@ -288,6 +278,15 @@ extension HTTPNetworkTransport: NetworkTransport {
}
}

// MARK: - UploadingNetworkTransport conformance

extension HTTPNetworkTransport: UploadingNetworkTransport {

public func upload<Operation>(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable {
return send(operation: operation, files: files, completionHandler: completionHandler)
}
}

// MARK: - Equatable conformance

extension HTTPNetworkTransport: Equatable {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Apollo/JSONSerializationFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

public final class JSONSerializationFormat {
public class func serialize(value: JSONEncodable) throws -> Data {
return try JSONSerialization.data(withJSONObject: value.jsonValue, options: [])
return try JSONSerialization.dataSortedIfPossible(withJSONObject: value.jsonValue)
}

public class func deserialize(data: Data) throws -> JSONValue {
Expand Down
13 changes: 13 additions & 0 deletions Sources/Apollo/NetworkTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,16 @@ public protocol NetworkTransport {
/// - Returns: An object that can be used to cancel an in progress request.
func send<Operation>(operation: Operation, completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable
}

/// A network transport which can also handle uploads of files.
public protocol UploadingNetworkTransport: NetworkTransport {

/// Uploads the given files with the given operation.
///
/// - Parameters:
/// - operation: The operation to send
/// - files: An array of `GraphQLFile` objects to send.
/// - completionHandler: The completion handler to execute when the request completes or errors
/// - Returns: An object that can be used to cancel an in progress request.
func upload<Operation>(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable
}
75 changes: 58 additions & 17 deletions Sources/Apollo/RequestCreator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,68 @@ public struct RequestCreator {
return body
}

static func requestMultipartFormData<Operation: GraphQLOperation>(for operation: Operation, files: [GraphQLFile], sendOperationIdentifiers: Bool, serializationFormat: JSONSerializationFormat.Type) throws -> MultipartFormData {
let formData = MultipartFormData()

let fields = requestBody(for: operation, sendOperationIdentifiers: sendOperationIdentifiers)
for (name, data) in fields {
if let data = data as? GraphQLMap {
let data = try serializationFormat.serialize(value: data)
formData.appendPart(data: data, name: name)
} else if let data = data as? String {
try formData.appendPart(string: data, name: name)
/// Creates multi-part form data to send with a request
///
/// - Parameters:
/// - operation: The operation to create the data for.
/// - files: An array of files to use.
/// - sendOperationIdentifiers: True if operation identifiers should be sent, false if not.
/// - serializationFormat: The format to use to serialize data.
/// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise.
/// - Returns: The created form data
/// - Throws: Errors creating or loading the form data
static func requestMultipartFormData<Operation: GraphQLOperation>(for operation: Operation,
files: [GraphQLFile],
sendOperationIdentifiers: Bool,
serializationFormat: JSONSerializationFormat.Type,
manualBoundary: String? = nil) throws -> MultipartFormData {
let formData: MultipartFormData

if let boundary = manualBoundary {
formData = MultipartFormData(boundary: boundary)
} else {
formData = MultipartFormData()
}

// Make sure all fields for files are set to null, or the server won't look
// for the files in the rest of the form data
let fieldsForFiles = Set(files.map { $0.fieldName })
var fields = requestBody(for: operation, sendOperationIdentifiers: sendOperationIdentifiers)
var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap()
for fieldName in fieldsForFiles {
if
let value = variables[fieldName],
let arrayValue = value as? [JSONEncodable] {
let updatedArray: [JSONEncodable?] = arrayValue.map { _ in nil }
variables.updateValue(updatedArray, forKey: fieldName)
} else {
try formData.appendPart(string: data.debugDescription, name: name)
variables.updateValue(nil, forKey: fieldName)
}
}
fields["variables"] = variables

let operationData = try serializationFormat.serialize(value: fields)
formData.appendPart(data: operationData, name: "operations")

var map = [String: [String]]()
if files.count == 1 {
let firstFile = files.first!
map["0"] = ["variables.\(firstFile.fieldName)"]
} else {
for (index, file) in files.enumerated() {
map["\(index)"] = ["variables.\(file.fieldName).\(index)"]
}
}

let mapData = try serializationFormat.serialize(value: map)
formData.appendPart(data: mapData, name: "map")

files.forEach {
formData.appendPart(inputStream: $0.inputStream,
contentLength: $0.contentLength,
name: $0.fieldName,
contentType: $0.mimeType,
filename: $0.originalName)
for (index, file) in files.enumerated() {
formData.appendPart(inputStream: file.inputStream,
contentLength: file.contentLength,
name: "\(index)",
contentType: file.mimeType,
filename: file.originalName)
}

return formData
Expand Down
15 changes: 12 additions & 3 deletions Sources/ApolloWebSocket/SplitNetworkTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import Apollo

/// A network transport that sends subscriptions using one `NetworkTransport` and other requests using another `NetworkTransport`. Ideal for sending subscriptions via a web socket but everything else via HTTP.
public class SplitNetworkTransport {
private let httpNetworkTransport: NetworkTransport
private let httpNetworkTransport: UploadingNetworkTransport
private let webSocketNetworkTransport: NetworkTransport

/// Designated initializer
///
/// - Parameters:
/// - httpNetworkTransport: A `NetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar.
/// - httpNetworkTransport: An `UploadingNetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar.
/// - webSocketNetworkTransport: A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar.
public init(httpNetworkTransport: NetworkTransport, webSocketNetworkTransport: NetworkTransport) {
public init(httpNetworkTransport: UploadingNetworkTransport, webSocketNetworkTransport: NetworkTransport) {
self.httpNetworkTransport = httpNetworkTransport
self.webSocketNetworkTransport = webSocketNetworkTransport
}
Expand All @@ -30,3 +30,12 @@ extension SplitNetworkTransport: NetworkTransport {
}
}
}

// MARK: - UploadingNetworkTransport conformance

extension SplitNetworkTransport: UploadingNetworkTransport {

public func upload<Operation>(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable {
return httpNetworkTransport.upload(operation: operation, files: files, completionHandler: completionHandler)
}
}
20 changes: 16 additions & 4 deletions Tests/ApolloPerformanceTests/NormalizedCachingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,14 @@ class NormalizedCachingTests: XCTestCase {
(1...100).forEach { number in
let expectation = self.expectation(description: "Loading query #\(number) from store")

store.load(query: query) { (result, error) in
XCTAssertEqual(result?.data?.hero?.name, "R2-D2")
store.load(query: query) { result in
switch result {
case .success(let graphQLResult):
XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2")
case .failure(let error):
XCTFail("Unexpected error: \(error)")
}

expectation.fulfill()
}
}
Expand Down Expand Up @@ -111,8 +117,14 @@ class NormalizedCachingTests: XCTestCase {
(1...10).forEach { _ in
let expectation = self.expectation(description: "Loading query #\(number) from store")

store.load(query: query) { (result, error) in
XCTAssertEqual(result?.data?.hero?.friends?.first??.name, "Droid #\(number)")
store.load(query: query) { result in
switch result {
case .success(let graphQLResult):
XCTAssertEqual(graphQLResult.data?.hero?.friends?.first??.name, "Droid #\(number)")
case .failure(let error):
XCTFail("Unexpected error: \(error)")
}

expectation.fulfill()
}
}
Expand Down
Loading