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

Fix for multi-variable uploads #1279

Merged
merged 2 commits into from
Jun 30, 2020
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
33 changes: 23 additions & 10 deletions Sources/Apollo/RequestCreator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,15 @@ extension RequestCreator {

// 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 })
let fieldsForFiles = Set(files.map { $0.fieldName }).sorted()
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)
let arrayOfNils: [JSONEncodable?] = arrayValue.map { _ in nil }
variables.updateValue(arrayOfNils, forKey: fieldName)
} else {
variables.updateValue(nil, forKey: fieldName)
}
Expand All @@ -125,20 +125,33 @@ extension RequestCreator {
let operationData = try serializationFormat.serialize(value: fields)
formData.appendPart(data: operationData, name: "operations")

// If there are multiple files for the same field, make sure to include them with indexes for the field. If there are multiple files for different fields, just use the field name.
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)"]
var currentIndex = 0

var sortedFiles = [GraphQLFile]()
for fieldName in fieldsForFiles {
let filesForField = files.filter { $0.fieldName == fieldName }
if filesForField.count == 1 {
let firstFile = filesForField.first!
map["\(currentIndex)"] = ["variables.\(firstFile.fieldName)"]
sortedFiles.append(firstFile)
currentIndex += 1
} else {
for (index, file) in filesForField.enumerated() {
map["\(currentIndex)"] = ["variables.\(file.fieldName).\(index)"]
sortedFiles.append(file)
currentIndex += 1
}
}
}

assert(sortedFiles.count == files.count, "Number of sorted files did not equal the number of incoming files - some field name has been left out.")

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

for (index, file) in files.enumerated() {
for (index, file) in sortedFiles.enumerated() {
formData.appendPart(inputStream: try file.generateInputStream(),
contentLength: file.contentLength,
name: "\(index)",
Expand Down
90 changes: 88 additions & 2 deletions Tests/ApolloTests/RequestCreatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ class RequestCreatorTests: XCTestCase {
private let customRequestCreator = TestCustomRequestCreator()
private let apolloRequestCreator = ApolloRequestCreator()



private func checkString(_ string: String,
includes expectedString: String,
file: StaticString = #file,
Expand Down Expand Up @@ -289,6 +287,94 @@ Bravo file content.
}
}

func testMultipleFilesWithMultipleFieldsWithApolloRequestCreator() throws {
let alphaFileURL = self.fileURLForFile(named: "a", extension: "txt")
let alphaFile = try GraphQLFile(fieldName: "uploads",
originalName: "a.txt",
mimeType: "text/plain",
fileURL: alphaFileURL)

let betaFileURL = self.fileURLForFile(named: "b", extension: "txt")
let betaFile = try GraphQLFile(fieldName: "uploads",
originalName: "b.txt",
mimeType: "text/plain",
fileURL: betaFileURL)

let charlieFileUrl = self.fileURLForFile(named: "c", extension: "txt")
let charlieFile = try GraphQLFile(fieldName: "secondField",
originalName: "c.txt",
mimeType: "text/plain",
fileURL: charlieFileUrl)

let data = try apolloRequestCreator.requestMultipartFormData(
for: HeroNameQuery(),
files: [alphaFile, betaFile, charlieFile],
sendOperationIdentifiers: false,
serializationFormat: JSONSerializationFormat.self,
manualBoundary: "TEST.BOUNDARY"
)

let stringToCompare = try self.string(from: data)

if JSONSerialization.dataCanBeSorted() {
let expectedString = """
--TEST.BOUNDARY
Content-Disposition: form-data; name="operations"

{"operationName":"HeroName","query":"query HeroName($episode: Episode) {\\n hero(episode: $episode) {\\n __typename\\n name\\n }\\n}","variables":{"episode":null,\"secondField\":null,\"uploads\":null}}
--TEST.BOUNDARY
Content-Disposition: form-data; name="map"

{"0":["variables.secondField"],"1":["variables.uploads.0"],"2":["variables.uploads.1"]}
--TEST.BOUNDARY
Content-Disposition: form-data; name="0"; filename="c.txt"
Content-Type: text/plain

Charlie file content.

--TEST.BOUNDARY
Content-Disposition: form-data; name="1"; filename="a.txt"
Content-Type: text/plain

Alpha file content.

--TEST.BOUNDARY
Content-Disposition: form-data; name="2"; filename="b.txt"
Content-Type: text/plain

Bravo file content.

--TEST.BOUNDARY--
"""
XCTAssertEqual(stringToCompare, expectedString)
} else {
// Query and operation parameters may be in weird order, so let's at least check that the files got encoded properly.
let endString = """
--TEST.BOUNDARY
Content-Disposition: form-data; name="0"; filename="c.txt"
Content-Type: text/plain

Charlie file content.

--TEST.BOUNDARY
Content-Disposition: form-data; name="1"; filename="a.txt"
Content-Type: text/plain

Alpha file content.

--TEST.BOUNDARY
Content-Disposition: form-data; name="2"; filename="b.txt"
Content-Type: text/plain

Bravo file content.

--TEST.BOUNDARY--
"""
self.checkString(stringToCompare, includes: endString)
}
}


func testRequestBodyWithApolloRequestCreator() {
let query = HeroNameQuery()
let req = apolloRequestCreator.requestBody(for: query, sendOperationIdentifiers: false)
Expand Down