Currently I have a mutation like this:
mutation uploadFile($file:Upload!) {
}
extension UploadFileMutation {
public typealias Upload = GraphQLFile
}
I've made an extension, so the compiler doesn't complain about the type
When I try use GraphQLFile as parameter
Cannot convert value of type 'UploadFileMutation.Upload' (aka 'GraphQLFile') to expected dictionary value type 'Optional
I couldn't upload with HTTPTransportNetwork as well.. Could someone give some hints?
Thanks!
This is related to #681 (missing documentation).
If you take a look at the expected string in the multi-part form data and the docs for the multi-part upload spec you can see the type of query you'd want to have is something like this:
mutation ($file: Upload!) {
singleUpload(file: $file) {
id
}
}
That singleUpload is the name of the method your schema provides for uploading the file. The upload method should take care of doing all the multipart handling nonsense for you.
Thanks a lot for your help, Ellen... I hope the example arrive soon, I still don't have success sending GraphQLFile to a GraphQL on a rails server...
Cheers!
Cleverson
Is the issue you're having on the server side? If so, can we close this issue out?
If not, can you be more specific about what's going wrong?
To be honest, I'm not sure if is server side issue...
I'm doing:
let file= GraphQLFile(fieldName: "file", originalName: "file", mimeType: "image/png", data: imageData!)
let mutation = UploadFileMutation(file: file)
I then create an extension
extension UploadFileMutation {
public typealias Upload = GraphQLFile
}
Then in this line,
public var variables: GraphQLMap? {
return ["file": file]
}
I get this error
Cannot convert value of type 'UploadFileMutation.Upload' (aka 'GraphQLFile') to expected dictionary value type 'Optional
I'm totally lost!
Thanks a lot, Ellen
You should not need to create that typealias yourself - can you post the generated code for UploadFileMutation please?
Sure! Thanks a lot, Ellen! Cheers!
public final class UploadFileMutation: GraphQLMutation {
public let operationDefinition =
"mutation UploadFile($file: Upload!) {\n uploadFile(input: {file: $file}) {\n __typename\n attachment {\n __typename\n id\n }\n errors\n }\n}"
public let operationName = "UploadFile"
public var file: Upload
public init(file: Upload) {
self.file = file
}
public var variables: GraphQLMap? {
return ["file": file]
}
public struct Data: GraphQLSelectionSet {
public static let possibleTypes = ["Mutation"]
public static let selections: [GraphQLSelection] = [
GraphQLField("uploadFile", arguments: ["input": ["file": GraphQLVariable("file")]], type: .object(UploadFile.selections)),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(uploadFile: UploadFile? = nil) {
self.init(unsafeResultMap: ["__typename": "Mutation", "uploadFile": uploadFile.flatMap { (value: UploadFile) -> ResultMap in value.resultMap }])
}
public var uploadFile: UploadFile? {
get {
return (resultMap["uploadFile"] as? ResultMap).flatMap { UploadFile(unsafeResultMap: $0) }
}
set {
resultMap.updateValue(newValue?.resultMap, forKey: "uploadFile")
}
}
public struct UploadFile: GraphQLSelectionSet {
public static let possibleTypes = ["UploadFilePayload"]
public static let selections: [GraphQLSelection] = [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("attachment", type: .object(Attachment.selections)),
GraphQLField("errors", type: .list(.nonNull(.scalar(String.self)))),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(attachment: Attachment? = nil, errors: [String]? = nil) {
self.init(unsafeResultMap: ["__typename": "UploadFilePayload", "attachment": attachment.flatMap { (value: Attachment) -> ResultMap in value.resultMap }, "errors": errors])
}
public var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}
public var attachment: Attachment? {
get {
return (resultMap["attachment"] as? ResultMap).flatMap { Attachment(unsafeResultMap: $0) }
}
set {
resultMap.updateValue(newValue?.resultMap, forKey: "attachment")
}
}
public var errors: [String]? {
get {
return resultMap["errors"] as? [String]
}
set {
resultMap.updateValue(newValue, forKey: "errors")
}
}
public struct Attachment: GraphQLSelectionSet {
public static let possibleTypes = ["Attachment"]
public static let selections: [GraphQLSelection] = [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("id", type: .nonNull(.scalar(GraphQLID.self))),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(id: GraphQLID) {
self.init(unsafeResultMap: ["__typename": "Attachment", "id": id])
}
public var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}
public var id: GraphQLID {
get {
return resultMap["id"]! as! GraphQLID
}
set {
resultMap.updateValue(newValue, forKey: "id")
}
}
}
}
}
}
Hi!
Hope this can help
This is my mutation
mutation AddImage($id: ID!, $image: ImageInput!) {
addImage(id: $id, image: $image) {
...ImageFragment
}
}
This is my swift method:
@discardableResult
func addImage(id: String,
image: Data,
source: String?,
copyright: String,
completion: @escaping (Swift.Result<ImageFragment, SupertrendsClientError>) -> Void) -> Cancellable {
let name = UUID().uuidString
let file = GraphQLFile(fieldName: name, originalName: name, mimeType: "image/jpeg", data: image)
let imageInput = ImageInput(copyright: copyright, source: source, upload: name, url: nil)
let query = AddImageMutation(id: id, image: imageInput)
return performUpload(query: query, files: [file], resultMap: { $0.addImage?.fragments.imageFragment }, completion: completion)
}
performUpload is just a generic method for sharing the upload function.
I have also added a type alias for my Upload file public typealias Upload = String
@kimdv Thanks a lot friend! I'll try that!
One question the performUpload you've talked about just execute te mutation within Apollo? Thanks!!!!!!!!
private func performUpload<Query: GraphQLMutation, Res>(query: Query,
files: [GraphQLFile],
resultMap: @escaping (Query.Data) -> Res?,
completion: @escaping (Swift.Result<Res, SupertrendsClientError>) -> Void) -> Cancellable {
return networkTransport.upload(operation: query, files: files) { result in
switch result {
case .success(let data):
if let object = try? Query.Data.init(jsonObject: data.body),
let model = resultMap(object) {
completion(.success(model))
} else {
completion(.failure(SupertrendsClientError.unknown))
}
case .failure(let error):
completion(.failure(SupertrendsClientError.unknown))
}
}
}
Here it is
And related to this.
@designatednerd the reason I first had added the upload method to ApolloClient was for the same result type. Now we get a more generic one, and not the same as "normal" mutation.
I think we should make is similar somehow
@kimdv Thanks a lot my friend, I'll try that now, I really appreciate your help!
Cheers!
@kdvtrifork Can you give me a little help with that?
I got a response from my backend developer, my mutation is now returning this, shouldn't the. be Content-Disposition: form-data, multipart-data instead?
2019-08-07T12:55:07.405171+00:00 app[web.1]: UploadFile
2019-08-07T12:55:07.405173+00:00 app[web.1]: --apollo-ios.boundary.0D94B585-41E9-42A6-B29F-7BBAE60AF397
2019-08-07T12:55:07.405175+00:00 app[web.1]: Content-Disposition: form-data; name="file"; filename="file"
2019-08-07T12:55:07.405176+00:00 app[web.1]: Content-Type: image/jpeg
////
let fileFieldName = "file"
let file = GraphQLFile(fieldName: fileFieldName, originalName: fileFieldName, mimeType: "image/jpeg", data: imageData)
let uploadInput = UploadFileMutation(file: fileFieldName)
let configuration = URLSessionConfiguration.default
let transport = HTTPNetworkTransport(url: AppParameters.APIPublicUrl, configuration: configuration)
let upload = transport.upload(operation: uploadInput, files: [file]) { (result) in
///////
}
Thanks a lot brother!
Cheers!
I don't understand the question?
The mutation and file creation looks good.
Is the server not accepting it?
@kimdv On the server, instead of multipart / form-data, the server is getting:
Content-Disposition: form-data; name="file"; filename="file"
The implication is based on this specification
Does the server also follow that?
@kdvtrifork I believe so, when I'm using Altair GraphQL, I'm able to perform uploads...
My code is this, maybe I'm still doing something wrong? Many many thanks, Kim!
if let imageData = image.jpegData(compressionQuality: 0.7) {
self.UIManager.disableUI()
let fileFieldName = "file"
let file = GraphQLFile(fieldName: fileFieldName, originalName: fileFieldName, mimeType: "image/jpeg", data: imageData)
let uploadInput = UploadFileInput(clientId: AppParameters.clientId,
clientSecret: AppParameters.clientSecret,
file: fileFieldName)
let uploadMutation = UploadFileMutation(input: uploadInput)
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"Language" : "en"
]
let transport = HTTPNetworkTransport(url: AppParameters.APIPublicUrl, configuration: configuration)
let upload = transport.upload(operation: uploadMutation, files: [file]) { (result) in
self.UIManager.enableUI()
switch result {
case .success(let data):
print("Success")
dump(data)
case .failure(let error):
print("Error \(error.localizedDescription)")
}
}
}
Looks like Altair is using the FormData type to do uploads - under the hood, it looks like that's using multipart/form-data rather than just form-data.
I would talk to your backend person about the spec - all of the examples there use form-data rather than multipart/form-data.
Hi Ellen... The awkward thing is that the CURL example from specifications work with our backend...
curl http://madari-staging.herokuapp.com/graphql
-F operations='{ "query": "mutation($image: Upload!, $clientId: String!, $clientSecret: String!) { uploadFile( input: { file: $image clientId: $clientId, clientSecret: $clientSecret } ) { errors attachment { id } } }", "variables": { "clientId": "fGwoCunPSgA0uDnaW8I_NhMtOemN-zYuEb6HKaMlfvs", "clientSecret": "wFQkIOYzrAh6ulcJLTEvKQjW4aP5hBAPWzNxw5j7pQw", "image": null } }'
-F map='{ "0": ["variables.image"] }'
-F 0=@oren\ Logo.png
Thanks again!
The "back-end guy" checking in here to help my co-work @Cleversou1983 on this issue.
AFAIK the back-end should follow the specs as we are using the suggested jetruby/apollo_upload_server-ruby ruby gem.
The spec shows a cURL example we could successfully run (as above) so maybe it helps lighten up the problem here. That is a public available version we can all use if it helps.
Comparing the server log between the cURL and the request coming from the iOS client show an interesting difference:
cURL shows:
[2019-08-07T18:15:45.805101 #4] INFO -- : [cf2ffd25-3124-4752-81df-fcee3ce30cd1] Processing by Public::GraphqlController#execute as */*
[2019-08-07T18:15:45.805334 #4] INFO -- : [cf2ffd25-3124-4752-81df-fcee3ce30cd1] Parameters: {"operations"=>"{ \"query\": \"mutation($image: Upload!, $clientId: String!, $clientSecret: String!) { uploadFile( input: { file: $image clientId: $clientId, clientSecret: $clientSecret } ) { errors attachment { id } } }\", \"variables\": { \"clientId\": \"fGwoCunPSgA0uDnaW8I_NhMtOemN-zYuEb6HKaMlfvs\", \"clientSecret\": \"wFQkIOYzrAh6ulcJLTEvKQjW4aP5hBAPWzNxw5j7pQw\", \"image\": null } }", "map"=>"{ \"0\": [\"variables.image\"] }", "0"=>#<ActionDispatch::Http::UploadedFile:0x00007ffa8c869f50 @tempfile=#<Tempfile:/tmp/RackMultipart20190807-4-16tyea8.png>, @original_filename="Oren Logo.png", @content_type="application/octet-stream", @headers="Content-Disposition: form-data; name=\"0\"; filename=\"Oren Logo.png\"\r\nContent-Type: application/octet-stream\r\n">, "query"=>"mutation($image: Upload!, $clientId: String!, $clientSecret: String!) { uploadFile( input: { file: $image clientId: $clientId, clientSecret: $clientSecret } ) { errors attachment { id } } }", "variables"=>{"clientId"=>"fGwoCunPSgA0uDnaW8I_NhMtOemN-zYuEb6HKaMlfvs", "clientSecret"=>"wFQkIOYzrAh6ulcJLTEvKQjW4aP5hBAPWzNxw5j7pQw", "image"=>#<ActionDispatch::Http::UploadedFile:0x00007ffa8c869f50 @tempfile=#<Tempfile:/tmp/RackMultipart20190807-4-16tyea8.png>, @original_filename="Oren Logo.png", @content_type="application/octet-stream", @headers="Content-Disposition: form-data; name=\"0\"; filename=\"Oren Logo.png\"\r\nContent-Type: application/octet-stream\r\n">}}
........
[2019-08-07T18:15:46.777065 #4] INFO -- : [cf2ffd25-3124-4752-81df-fcee3ce30cd1] [ActiveJob] Enqueued ActiveStorage::AnalyzeJob (Job ID: 3a71cfa8-e599-49ea-b013-da68f3e26ea3) to Async(default) with arguments: #<GlobalID:0x00007ffa8c6c1bd0 @uri=#<URI::GID gid://madari/ActiveStorage::Blob/58>>
[2019-08-07T18:15:46.779570 #4] INFO -- : [cf2ffd25-3124-4752-81df-fcee3ce30cd1] Completed 200 OK in 974ms (Views: 0.3ms | ActiveRecord: 18.6ms)
Request from iOS shows:
... A LOT MORE OF THESE ODD CHARACTERS
2019-08-07T18:14:07.336809+00:00 app[web.1]: ��r��X��F�y2��+g$�
2019-08-07T18:14:07.336810+00:00 app[web.1]: rZ�>�������<p=��G$Z6�� �\�TN�d
���o���PG�r�f
2019-08-07T18:14:07.336812+00:00 app[web.1]: m�|�6�rM2�FLO��
2019-08-07T18:14:07.336813+00:00 app[web.1]: ���1C�6s��|�w�cN���[�zW8�3��[���s��$"3!<��c�Cn%
�N@�닄�qԚd�a2�禘J�y�3d�.��ϔ��cV�7w�8�3�9�!,2�c�2H��\m�k�x=+��J�v�������:���8�37SVev�0Gzʸ|-|�G�ެ�#��'9����y�7�S�f��-Up�r�㹪N�d�G���J�v
2019-08-07T18:14:07.336815+00:00 app[web.1]: rEr�G��B�[��
2019-08-07T18:14:07.336816+00:00 app[web.1]: --apollo-ios.boundary.0C6D05A8-D24A-4710-AAB3-A5B17932B9BF--
2019-08-07T18:14:07.337714+00:00 app[web.1]: F, [2019-08-07T18:14:07.337599 #4] FATAL -- : [0be539e3-58c8-40a6-8e3f-191a69c56806]
2019-08-07T18:14:07.337783+00:00 app[web.1]: F, [2019-08-07T18:14:07.337729 #4] FATAL -- : [0be539e3-58c8-40a6-8e3f-191a69c56806] ActionDispatch::Http::Parameters::ParseError (765: unexpected token at '--apollo-ios.boundary.0C6D05A8-D24A-4710-AAB3-A5B17932B9BF
2019-08-07T18:14:07.337785+00:00 app[web.1]: Content-Disposition: form-data; name="query"
2019-08-07T18:14:07.337787+00:00 app[web.1]:
2019-08-07T18:14:07.337788+00:00 app[web.1]: mutation UploadFile($image: Upload!, $clientID: String!, $clientSecret: String!) {
2019-08-07T18:14:07.337790+00:00 app[web.1]: uploadFile(input: {file: $image, clientId: $clientID, clientSecret: $clientSecret}) {
2019-08-07T18:14:07.337792+00:00 app[web.1]: __typename
2019-08-07T18:14:07.337793+00:00 app[web.1]: attachment {
2019-08-07T18:14:07.337795+00:00 app[web.1]: __typename
2019-08-07T18:14:07.337796+00:00 app[web.1]: id
2019-08-07T18:14:07.337798+00:00 app[web.1]: }
2019-08-07T18:14:07.337799+00:00 app[web.1]: errors
2019-08-07T18:14:07.337800+00:00 app[web.1]: }
2019-08-07T18:14:07.337802+00:00 app[web.1]: }
2019-08-07T18:14:07.337804+00:00 app[web.1]: --apollo-ios.boundary.0C6D05A8-D24A-4710-AAB3-A5B17932B9BF
2019-08-07T18:14:07.337805+00:00 app[web.1]: Content-Disposition: form-data; name="variables"
2019-08-07T18:14:07.337807+00:00 app[web.1]:
2019-08-07T18:14:07.337808+00:00 app[web.1]: {"image":"","clientID":"k6PPvCVbzpCuXik5WMpSLoIcPWqbIK-Ot5Rm7Z8cM6E","clientSecret":"YlxQChkVtQ5rzeGh0FdzfsQJVxHlOHFICfNg5QcQK8g"}
2019-08-07T18:14:07.337810+00:00 app[web.1]: --apollo-ios.boundary.0C6D05A8-D24A-4710-AAB3-A5B17932B9BF
2019-08-07T18:14:07.337811+00:00 app[web.1]: Content-Disposition: form-data; name="operationName"
2019-08-07T18:14:07.337813+00:00 app[web.1]:
2019-08-07T18:14:07.337814+00:00 app[web.1]: UploadFile
2019-08-07T18:14:07.337816+00:00 app[web.1]: --apollo-ios.boundary.0C6D05A8-D24A-4710-AAB3-A5B17932B9BF
2019-08-07T18:14:07.337817+00:00 app[web.1]: Content-Disposition: form-data; name="file"; filename="file"
2019-08-07T18:14:07.337819+00:00 app[web.1]: Content-Type: image/jpeg
2019-08-07T18:14:07.337820+00:00 app[web.1]:
2019-08-07T18:14:07.337822+00:00 app[web.1]: ����'):
2019-08-07T18:14:07.337826+00:00 app[web.1]: F, [2019-08-07T18:14:07.337787 #4] FATAL -- : [0be539e3-58c8-40a6-8e3f-191a69c56806]
2019-08-07T18:14:07.337917+00:00 app[web.1]: F, [2019-08-07T18:14:07.337848 #4] FATAL -- : [0be539e3-58c8-40a6-8e3f-191a69c56806] vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.3/lib/action_dispatch/http/parameters.rb:117:in `rescue in parse_formatted_parameters'
2019-08-07T18:14:07.337920+00:00 app[web.1]: [0be539e3-58c8-40a6-8e3f-191a69c56806] vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.3/lib/action_dispatch/http/parameters.rb:111:in `parse_formatted_parameters'
2019-08-07T18:14:07.337922+00:00 app[web.1]: [0be539e3-58c8-40a6-8e3f-191a69c56806] vendor/bundle/ruby/2.5.0/gems/actionpack-5.2.3/lib/action_dispatch/http/request.rb:381:in `block in POST'
2019-08-07T18:14:07.337925+00:00 app[web.1]: [0be539e3-58c8-40a6-8e3f-191a69c56806] vendor/bundle/ruby/2.5.0/gems/rack-2.0.7/lib/rack/request.rb:59:in `fetch'
2019-08-07T18:14:07.337927+00:00 app[web.1]: [0be539e3-58c8-40a6-8
So im not sure what is the problem here but there is something strange if the request... Again, the above CURL should work publicly if it helps us debug. I thing the server is ok...
hope it help guys, thanks!
OK so I see a couple possibilities here:
boundary coming through correctly with the header? The "unexpected token" seems like maybe it's related to that. curl is uploading the data as application/octet-stream rather than image/jpg. What happens if you change the MIME type of the GraphQLFile to that? OK - well, I figured at least one thing out that I'll be fixing - it looks like the Content-Type is getting hammered with application/json even after it's been set.
@kimdv I could use your help on another bit - looking at what comes out of using RequestCreator.requestMultipartFormData, I'm not able to see where things are getting converted to a map like you're using in the multipart-form tests. Did I break something when I pulled that out into a separate piece?
@designatednerd There is something a line 37.
That should convert a GraphQLMap.
But I also thing that map is just a name.
But correct me if I'm wrong! 🤷♂️
Yeah I've been working on testing with yoga's upload engine and it would reject anything that didn't have a section explicitly named map.
Working on a PR.
hmm okay!
We use absinthe for our backend. If that can help..
For everyone here: #707 should help significantly with a lot of these issues.
Significant changes shipping with 0.15.0 - @Cleversou1983 if you're still having trouble after that upgrade and you can't work it out with your BE dev, please open a new issue and we'll try to figure it out.
I need to upload image with following mutation in swift 5.0
mutation updateUserProfile ($userId:ID!,$userImage:Upload,$userDetail:UserInput!){
updateUser(id:$userId,file:$userImage,user:$userDetail){
fname
lname
}
}
Can you please let me know how can I send a image data in $file
@DeekshaApptunix You should use GraphQLFile, like this:
let photoData = photo.jpegData(compressionQuality: 0.8)!
let file = GraphQLFile(fieldName: "file", originalName: "file.jpg", mimeType: "image/jpeg", data: photoData)
In your mutation, pass as the file parameter, the name of the parameter, for example:
updateUser(id:$userId,file:"file",user:$userDetail)
@Cleversou1983 If I am sending data like you suggested then I am also getting error
if let imageData = userImgVew.image?.jpegData(compressionQuality: 0.7) {
let file = GraphQLFile(fieldName: name, originalName: name, mimeType: "image/jpeg", data:imageData)
apollo.perform(mutation:UpdateUserProfileMutation.init(userId:id, userImage:"(file)", userDetail: userDict))
Error : "createReadStream is not a function"
fieldName needs to be the name of the field being uploaded, and you need to use the upload function rather than the perform(mutation: function in order to perform an upload.
I need to upload in this mutation
if let logoData = btnCommunity.currentImage?.jpegData(compressionQuality: 0.8) , let coverData = coverImage.image?.jpegData(compressionQuality: 0.8) {
let coverFile = GraphQLFile(fieldName:"file", originalName:"cover.png", mimeType: "image/jpeg", data:coverData)
let logoFile = GraphQLFile(fieldName:"logo", originalName:"logo.png", mimeType: "image/jpeg", data:logoData)
}
let uploadData = CreateCommunityMutation(communityDetail:communityDetail, invitedusers: invitedUser, file: "(file)", communityLogo: "(logo)")
apollo.upload(operation:uploadData, files:[]){ result in
Here is my mutation for the same
mutation createCommunity($communityDetail:CommunityInput!,$invitedusers:[ID],$file:Upload,$communityLogo:Upload){
createCommunity(community:$communityDetail , users:$invitedusers , file:$file , logo:$communityLogo ){
id
name
}
}
I am getting following error
createReadStream is not a function
@DeekshaApptunix let's try to concentrate your problems in issue #938 please. Thank you!
Most helpful comment
For everyone here: #707 should help significantly with a lot of these issues.