Performing mutations


In addition to fetching data using queries, Apollo iOS also handles GraphQL mutations. Mutations are identical to queries in syntax, the only difference being that you use the keyword mutation instead of query to indicate that the root fields on this query are going to be performing writes to the backend.

GraphQL
1mutation UpvotePost($postId: Int!) {
2  upvotePost(postId: $postId) {
3    votes
4  }
5}
6

GraphQL mutations represent two things in one query string:

  1. The mutation field name with arguments, upvotePost, which represents the actual operation to be done on the server

  2. The fields you want back from the result of the mutation to update the client: { votes }

The above mutation will upvote a post on the server. The result might be:

Text
1{
2  "data": {
3    "upvotePost": {
4      "id": "123",
5      "votes": 5
6    }
7  }
8}

Similar to queries, mutations are represented by instances of generated classes, conforming to the GraphQLMutation protocol. Constructor arguments are used to define mutation variables. You pass a mutation object to ApolloClient#perform(mutation:) to send the mutation to the server, execute it, and receive typed results:

Swift
1apollo.perform(mutation: UpvotePostMutation(postId: postId)) { result in
2  guard let data = try? result.get().data else { return }
3  print(data.upvotePost?.votes)
4}

Using fragments in mutation results

In many cases, you'll want to use mutation results to update your UI. Fragments can be a great way of sharing result handling between queries and mutations:

GraphQL
1mutation UpvotePost($postId: Int!) {
2  upvotePost(postId: $postId) {
3    ...PostDetails
4  }
5}
Swift
1apollo.perform(mutation: UpvotePostMutation(postId: postId)) { result in
2  guard let data = try? result.get().data else { return }
3  self.configure(with: data.upvotePost?.fragments.postDetails)
4}

Passing input objects

The GraphQL type system includes input objects as a way to pass complex values to fields. Input objects are often defined as mutation variables, because they give you a compact way to pass in objects to be created:

GraphQL
1mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {
2  createReview(episode: $episode, review: $review) {
3    stars
4    commentary
5  }
6}
Swift
1let review = ReviewInput(stars: 5, commentary: "This is a great movie!")
2apollo.perform(mutation: CreateReviewForEpisodeMutation(episode: .jedi, review: review))

Designing mutation results

When people talk about GraphQL, they often focus on the data fetching side of things, because that's where GraphQL brings the most value. Mutations can be pretty nice if done well, but the principles of designing good mutations, and especially good mutation result types, are not yet well-understood in the open source community. So when you are working with mutations it might often feel like you need to make a lot of application-specific decisions.

In GraphQL, mutations can return any type, and that type can be queried just like a regular GraphQL query. So the question is - what type should a particular mutation return?

In most cases, the data available from a mutation result should be the server developer's best guess of the data a client would need to understand what happened on the server. For example, a mutation that creates a new comment on a blog post might return the comment itself. A mutation that reorders an array might need to return the whole array.

Uploading files

An Important Caveat About File Uploads

Apollo recommends only using GraphQL file uploading for proof-of-concept applications. While there is a spec we presently support for making multipart-form requests with GraphQL, we've found that in practice that it's much simpler to use more purpose-built tools for file upload.

In practice, this means using a more traditional method to upload your file like REST multipart-form uploads or SDK's that support file uploads, such as AmazonS3. This article covers how to do that with Typescript, but the general theory for iOS works basically the same:

  • Upload data not using GraphQL, getting back either an identifier or URL for the uploaded data.

  • Send that received identifier or URL to your graph using GraphQL.

If you'd still prefer to upload directly with Apollo, instructions follow.

Uploading Directly With Apollo

The iOS SDK supports the GraphQL Multipart Request Specification for uploading files.

At the moment, we only support uploads for a single operation, not for batch operations. You can upload multiple files for a single operation if your server supports it, though.

To upload a file, you will need:

  • A NetworkTransport which also supports the UploadingNetworkTransport protocol on your ApolloClient instance. If you're using RequestChainNetworkTransport (which is set up by default), this protocol is already supported.

  • The correct MIME type for the data you're uploading. The default value is application/octet-stream.

  • Either the data or the file URL of the data you want to upload.

  • A mutation which takes an Upload as a parameter. Note that this must be supported by your server.

Here is an example of a GraphQL query for a mutation that accepts a single upload, and then returns the id for that upload:

GraphQL
1mutation UploadFile($file:Upload!) {
2    singleUpload(file:$file) {
3        id
4    }
5}

If you wanted to use this to upload a file called a.txt, it would look something like this:

Swift
1// Create the file to upload
2guard
3  let fileURL = Bundle.main.url(forResource: "a", 
4                                withExtension: "txt"),
5  let file = GraphQLFile(fieldName: "file", // Must be the name of the field the file is being uploaded to
6                         originalName: "a.txt", 
7                         mimeType: "text/plain", // <-defaults to "application/octet-stream"
8                         fileURL: fileURL) else {
9    // Either the file URL couldn't be created or the file couldn't be created.
10    return 
11}
12
13// Actually upload the file
14client.upload(operation: UploadFileMutation(file: "a"), // <-- `Upload` is a custom scalar that's a `String` under the hood.
15              files: [file]) { result in 
16  switch result {
17  case .success(let graphQLResult):
18    print("ID: \(graphQLResult.data?.singleUpload.id)")
19  case .failure(let error):
20    print("error: \(error)")
21  }
22}

A few other notes:

  • Due to some limitations around the spec, whatever the file is being added for should be at the root of your GraphQL query. So if you have something like:

    GraphQL
    1mutation AvatarUpload($userID: GraphQLID!, $file: Upload!) {
    2  id
    3}

    it will work, but if you have some kind of object encompassing both of those fields like this:

    GraphQL
    1// Assumes AvatarObject(userID: GraphQLID, file: Upload) exists
    2mutation AvatarUpload($avatarObject: AvatarObject!) {
    3  id
    4}

    it will not. Generally you should be able to deconstruct upload objects to allow you to send the appropriate fields.

  • If you are uploading an array of files, you need to use the same field name for each file. These will be updated at send time.

  • If you are uploading an array of files, the array of Strings passed into the query must be the same number as the array of files.

Feedback

Edit on GitHub

Forums