Skip to content

Commit 426818a

Browse files
Merge pull request #707 from apollographql/add/upload-docs
Mo' Better Uploading
2 parents 609243c + fe0d847 commit 426818a

14 files changed

Lines changed: 492 additions & 154 deletions

File tree

Apollo.xcodeproj/project.pbxproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,7 @@
977977
isa = PBXProject;
978978
attributes = {
979979
LastSwiftUpdateCheck = 0830;
980-
LastUpgradeCheck = 1020;
980+
LastUpgradeCheck = 1030;
981981
ORGANIZATIONNAME = "Apollo GraphQL";
982982
TargetAttributes = {
983983
9F8A95771EC0FC1200304A2D = {

Apollo.xcodeproj/xcshareddata/xcschemes/Apollo.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1020"
3+
LastUpgradeVersion = "1030"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

Apollo.xcodeproj/xcshareddata/xcschemes/ApolloPerformanceTests.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1020"
3+
LastUpgradeVersion = "1030"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

ApolloSQLite.xcodeproj/xcshareddata/xcschemes/ApolloSQLite.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1020"
3+
LastUpgradeVersion = "1030"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

ApolloWebSocket.xcodeproj/xcshareddata/xcschemes/ApolloWebSocket.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1020"
3+
LastUpgradeVersion = "1030"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

Sources/Apollo/ApolloClient.swift

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ public class ApolloClient {
4040
private let queue: DispatchQueue
4141
private let operationQueue: OperationQueue
4242

43+
public enum ApolloClientError: Error, LocalizedError {
44+
case noUploadTransport
45+
46+
public var localizedDescription: String {
47+
switch self {
48+
case .noUploadTransport:
49+
return "Attempting to upload using a transport which does not support uploads. This is a developer error."
50+
}
51+
}
52+
}
53+
4354
/// Creates a client with the specified network transport and store.
4455
///
4556
/// - Parameters:
@@ -117,6 +128,30 @@ public class ApolloClient {
117128
@discardableResult public func perform<Mutation: GraphQLMutation>(mutation: Mutation, context: UnsafeMutableRawPointer? = nil, queue: DispatchQueue = DispatchQueue.main, resultHandler: GraphQLResultHandler<Mutation.Data>? = nil) -> Cancellable {
118129
return send(operation: mutation, shouldPublishResultToStore: true, context: context, resultHandler: wrapResultHandler(resultHandler, queue: queue))
119130
}
131+
132+
/// Uploads the given files with the given operation.
133+
///
134+
/// - Parameters:
135+
/// - operation: The operation to send
136+
/// - files: An array of `GraphQLFile` objects to send.
137+
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
138+
/// - completionHandler: The completion handler to execute when the request completes or errors
139+
/// - Returns: An object that can be used to cancel an in progress request.
140+
/// - Throws: If your `networkTransport` does nto also conform to `UploadingNetworkTransport`.
141+
@discardableResult public func upload<Operation: GraphQLOperation>(operation: Operation, context: UnsafeMutableRawPointer? = nil, files: [GraphQLFile], queue: DispatchQueue = .main, resultHandler: GraphQLResultHandler<Operation.Data>? = nil) -> Cancellable {
142+
let wrappedHandler = wrapResultHandler(resultHandler, queue: queue)
143+
guard let uploadingTransport = self.networkTransport as? UploadingNetworkTransport else {
144+
assertionFailure("Trying to upload without an uploading transport. Please make sure your network transport conforms to `UploadingNetworkTransport`.")
145+
wrappedHandler(.failure(ApolloClientError.noUploadTransport))
146+
return EmptyCancellable()
147+
}
148+
149+
return uploadingTransport.upload(operation: operation, files: files) { result in
150+
self.handleOperationResult(shouldPublishResultToStore: true,
151+
context: context, result,
152+
resultHandler: wrappedHandler)
153+
}
154+
}
120155

121156
/// Subscribe to a subscription
122157
///
@@ -132,33 +167,40 @@ public class ApolloClient {
132167

133168
fileprivate func send<Operation: GraphQLOperation>(operation: Operation, shouldPublishResultToStore: Bool, context: UnsafeMutableRawPointer?, resultHandler: @escaping GraphQLResultHandler<Operation.Data>) -> Cancellable {
134169
return networkTransport.send(operation: operation) { result in
135-
switch result {
136-
case .failure(let error):
137-
resultHandler(.failure(error))
138-
case .success(let response):
139-
// If there is no need to publish the result to the store, we can use a fast path.
140-
if !shouldPublishResultToStore {
141-
do {
142-
let result = try response.parseResultFast()
143-
resultHandler(.success(result))
144-
} catch {
145-
resultHandler(.failure(error))
146-
}
147-
return
170+
self.handleOperationResult(shouldPublishResultToStore: shouldPublishResultToStore,
171+
context: context,
172+
result,
173+
resultHandler: resultHandler)
174+
}
175+
}
176+
177+
private func handleOperationResult<Operation>(shouldPublishResultToStore: Bool, context: UnsafeMutableRawPointer?, _ result: Result<GraphQLResponse<Operation>, Error>, resultHandler: @escaping GraphQLResultHandler<Operation.Data>) {
178+
switch result {
179+
case .failure(let error):
180+
resultHandler(.failure(error))
181+
case .success(let response):
182+
// If there is no need to publish the result to the store, we can use a fast path.
183+
if !shouldPublishResultToStore {
184+
do {
185+
let result = try response.parseResultFast()
186+
resultHandler(.success(result))
187+
} catch {
188+
resultHandler(.failure(error))
148189
}
149-
150-
firstly {
151-
try response.parseResult(cacheKeyForObject: self.cacheKeyForObject)
152-
}.andThen { (result, records) in
153-
if let records = records {
154-
self.store.publish(records: records, context: context).catch { error in
155-
preconditionFailure(String(describing: error))
156-
}
190+
return
191+
}
192+
193+
firstly {
194+
try response.parseResult(cacheKeyForObject: self.cacheKeyForObject)
195+
}.andThen { (result, records) in
196+
if let records = records {
197+
self.store.publish(records: records, context: context).catch { error in
198+
preconditionFailure(String(describing: error))
157199
}
158-
resultHandler(.success(result))
159-
}.catch { error in
160-
resultHandler(.failure(error))
161-
}
200+
}
201+
resultHandler(.success(result))
202+
}.catch { error in
203+
resultHandler(.failure(error))
162204
}
163205
}
164206
}

Sources/Apollo/HTTPNetworkTransport.swift

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,7 @@ public class HTTPNetworkTransport {
9595
self.useGETForQueries = useGETForQueries
9696
self.delegate = delegate
9797
}
98-
99-
/// Uploads the given files with the given operation.
100-
///
101-
/// - Parameters:
102-
/// - operation: The operation to send
103-
/// - files: An array of `GraphQLFile` objects to send.
104-
/// - completionHandler: The completion handler to execute when the request completes or errors
105-
/// - Returns: An object that can be used to cancel an in progress request.
106-
public func upload<Operation>(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable {
107-
return send(operation: operation, files: files, completionHandler: completionHandler)
108-
}
109-
98+
11099
private func send<Operation>(operation: Operation, files: [GraphQLFile]?, completionHandler: @escaping (_ results: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable {
111100
let request: URLRequest
112101
do {
@@ -227,6 +216,9 @@ public class HTTPNetworkTransport {
227216
let body = RequestCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers)
228217
var request = URLRequest(url: self.url)
229218

219+
// We default to json, but this can be changed below if needed.
220+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
221+
230222
if self.useGETForQueries && operation.operationType == .query {
231223
let transformer = GraphQLGETTransformer(body: body, url: self.url)
232224
if let urlForGet = transformer.createGetURL() {
@@ -262,8 +254,6 @@ public class HTTPNetworkTransport {
262254
request.setValue(operationID, forHTTPHeaderField: "X-APOLLO-OPERATION-ID")
263255
}
264256

265-
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
266-
267257
// If there's a delegate, do a pre-flight check and allow modifications to the request.
268258
if
269259
let delegate = self.delegate,
@@ -288,6 +278,15 @@ extension HTTPNetworkTransport: NetworkTransport {
288278
}
289279
}
290280

281+
// MARK: - UploadingNetworkTransport conformance
282+
283+
extension HTTPNetworkTransport: UploadingNetworkTransport {
284+
285+
public func upload<Operation>(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable {
286+
return send(operation: operation, files: files, completionHandler: completionHandler)
287+
}
288+
}
289+
291290
// MARK: - Equatable conformance
292291

293292
extension HTTPNetworkTransport: Equatable {

Sources/Apollo/JSONSerializationFormat.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22

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

88
public class func deserialize(data: Data) throws -> JSONValue {

Sources/Apollo/NetworkTransport.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,16 @@ public protocol NetworkTransport {
99
/// - Returns: An object that can be used to cancel an in progress request.
1010
func send<Operation>(operation: Operation, completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable
1111
}
12+
13+
/// A network transport which can also handle uploads of files.
14+
public protocol UploadingNetworkTransport: NetworkTransport {
15+
16+
/// Uploads the given files with the given operation.
17+
///
18+
/// - Parameters:
19+
/// - operation: The operation to send
20+
/// - files: An array of `GraphQLFile` objects to send.
21+
/// - completionHandler: The completion handler to execute when the request completes or errors
22+
/// - Returns: An object that can be used to cancel an in progress request.
23+
func upload<Operation>(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result<GraphQLResponse<Operation>, Error>) -> Void) -> Cancellable
24+
}

Sources/Apollo/RequestCreator.swift

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,68 @@ public struct RequestCreator {
2828
return body
2929
}
3030

31-
static func requestMultipartFormData<Operation: GraphQLOperation>(for operation: Operation, files: [GraphQLFile], sendOperationIdentifiers: Bool, serializationFormat: JSONSerializationFormat.Type) throws -> MultipartFormData {
32-
let formData = MultipartFormData()
33-
34-
let fields = requestBody(for: operation, sendOperationIdentifiers: sendOperationIdentifiers)
35-
for (name, data) in fields {
36-
if let data = data as? GraphQLMap {
37-
let data = try serializationFormat.serialize(value: data)
38-
formData.appendPart(data: data, name: name)
39-
} else if let data = data as? String {
40-
try formData.appendPart(string: data, name: name)
31+
/// Creates multi-part form data to send with a request
32+
///
33+
/// - Parameters:
34+
/// - operation: The operation to create the data for.
35+
/// - files: An array of files to use.
36+
/// - sendOperationIdentifiers: True if operation identifiers should be sent, false if not.
37+
/// - serializationFormat: The format to use to serialize data.
38+
/// - manualBoundary: [optional] A manual boundary to pass in. A default boundary will be used otherwise.
39+
/// - Returns: The created form data
40+
/// - Throws: Errors creating or loading the form data
41+
static func requestMultipartFormData<Operation: GraphQLOperation>(for operation: Operation,
42+
files: [GraphQLFile],
43+
sendOperationIdentifiers: Bool,
44+
serializationFormat: JSONSerializationFormat.Type,
45+
manualBoundary: String? = nil) throws -> MultipartFormData {
46+
let formData: MultipartFormData
47+
48+
if let boundary = manualBoundary {
49+
formData = MultipartFormData(boundary: boundary)
50+
} else {
51+
formData = MultipartFormData()
52+
}
53+
54+
// Make sure all fields for files are set to null, or the server won't look
55+
// for the files in the rest of the form data
56+
let fieldsForFiles = Set(files.map { $0.fieldName })
57+
var fields = requestBody(for: operation, sendOperationIdentifiers: sendOperationIdentifiers)
58+
var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap()
59+
for fieldName in fieldsForFiles {
60+
if
61+
let value = variables[fieldName],
62+
let arrayValue = value as? [JSONEncodable] {
63+
let updatedArray: [JSONEncodable?] = arrayValue.map { _ in nil }
64+
variables.updateValue(updatedArray, forKey: fieldName)
4165
} else {
42-
try formData.appendPart(string: data.debugDescription, name: name)
66+
variables.updateValue(nil, forKey: fieldName)
4367
}
4468
}
69+
fields["variables"] = variables
70+
71+
let operationData = try serializationFormat.serialize(value: fields)
72+
formData.appendPart(data: operationData, name: "operations")
73+
74+
var map = [String: [String]]()
75+
if files.count == 1 {
76+
let firstFile = files.first!
77+
map["0"] = ["variables.\(firstFile.fieldName)"]
78+
} else {
79+
for (index, file) in files.enumerated() {
80+
map["\(index)"] = ["variables.\(file.fieldName).\(index)"]
81+
}
82+
}
83+
84+
let mapData = try serializationFormat.serialize(value: map)
85+
formData.appendPart(data: mapData, name: "map")
4586

46-
files.forEach {
47-
formData.appendPart(inputStream: $0.inputStream,
48-
contentLength: $0.contentLength,
49-
name: $0.fieldName,
50-
contentType: $0.mimeType,
51-
filename: $0.originalName)
87+
for (index, file) in files.enumerated() {
88+
formData.appendPart(inputStream: file.inputStream,
89+
contentLength: file.contentLength,
90+
name: "\(index)",
91+
contentType: file.mimeType,
92+
filename: file.originalName)
5293
}
5394

5495
return formData

0 commit comments

Comments
 (0)