Page updated Nov 8, 2023

Advanced Workflows

This section describes different use cases for constructing your own custom GraphQL requests and how to approach it. You may want to construct your own GraphQL request if you want to

  • retrieve only a subset of the data to reduce data transfer
  • retrieve nested objects at a depth that you choose
  • combine multiple operations into a single request
  • send custom headers to your AppSync endpoint

A GraphQL request is automatically generated for you when using AWSAPIPlugin with the existing workflow. For example, if you have a Todo model, a mutation request to save the Todo will look like this:

1let todo = Todo(name: "my first todo", description: "todo description")
2try await Amplify.API.mutate(request: .create(todo))

Underneath the covers, a request is generated with a GraphQL document and variables and sent to the AppSync service.

1{
2 "query": "mutation createTodo($input: CreateTodoInput!) {
3 createTodo(input: $input) {
4 id
5 name
6 description
7 }
8 }",
9 "variables": "{
10 "input": {
11 "id": "[UNIQUE-ID]",
12 "name": "my first todo",
13 "description": "todo description"
14 }
15 }
16}

The different parts of the document are described as follows

  • mutation - the operation type to be performed, other operation types are query and subscription
  • createTodo($input: CreateTodoInput!) - the name and input of the operation.
  • $input: CreateTodoInput! - the input of type CreateTodoInput! referencing the variables containing JSON input
  • createTodo(input: $input) - the mutation operation which takes a variable input from $input
  • the selection set containing id, name, and description are fields specified to be returned in the response

You can learn more about the structure of a request from GraphQL Query Language and AppSync documentation. To test out constructing your own requests, open the AppSync console using amplify console api and navigate to the Queries tab.

Subset of data

The selection set of the document specifies which fields are returned in the response. For example, if you are displaying a view of the Todo without the description, you can construct the document to omit the field. You can learn more about selection sets here.

1query getTodo($id: ID!) {
2 getTodo(id: $id) {
3 id
4 name
5 }
6}

The response data will look like this

1{
2 "data": {
3 "getTodo": {
4 "id": "111",
5 "name": "my first todo"
6 }
7 }
8}

First, create your own GraphQLRequest

1extension GraphQLRequest {
2 static func getWithoutDescription(byId id: String) -> GraphQLRequest<Todo> {
3 let operationName = "getTodo"
4 let document = """
5 query getTodo($id: ID!) {
6 \(operationName)(id: $id) {
7 id
8 name
9 }
10 }
11 """
12 return GraphQLRequest<Todo>(
13 document: document,
14 variables: ["id": id],
15 responseType: Todo.self,
16 decodePath: operationName
17 )
18 }
19}

The decode path specifies which part of the response to deserialize to the responseType. You'll need to specify the operation name to deserialize the object at "data.getTodo" successfully into a Todo model.

Then, query for the Todo by a todo id

1try await Amplify.API.query(request: .getWithoutDescription(byId: "[UNIQUE_ID]"))
2 // handle result

Custom Request Inputs

The GraphQLRequest's variables property takes in a JSON object. You can define this as an Encodable type and serialize it to a JSON object for the request.

Define the type that matches the expected input type from your GraphQL service.

1struct CreateTodoInput: Encodable {
2 let id: String
3 let name: String
4 let description: String?
5}

Then create the GraphQLRequest

1import Foundation
2
3extension GraphQLRequest {
4 static func createTodo(input: CreateTodoInput) throws -> GraphQLRequest<Todo> {
5 let operationName = "createTodo"
6 let document = """
7 query createTodo($input: CreateTodoInput!) {
8 \(operationName)(input: $input) {
9 id
10 name
11 description
12 }
13 }
14 """
15
16 let data = try JSONEncoder().encode(input)
17 let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
18
19 return GraphQLRequest<Todo>(
20 document: document,
21 variables: ["input": jsonObject],
22 responseType: Todo.self,
23 decodePath: operationName
24 )
25 }
26}

The input object is encoded as a data object and then serialized into the JSON object.

Nested Data

If you have a relational model, you can retrieve the nested object by creating a GraphQLRequest with a selection set containing the nested object's fields. For example, in this schema, the Post can contain multiple comments and notes.

1enum PostStatus {
2 ACTIVE
3 INACTIVE
4}
5
6type Post @model {
7 id: ID!
8 title: String!
9 rating: Int!
10 status: PostStatus!
11 comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"])
12 notes: [Note] @hasMany(indexName: "byNote", fields: ["id"])
13}
14
15type Comment @model {
16 id: ID!
17 postID: ID! @index(name: "byPost", sortKeyFields: ["content"])
18 post: Post! @belongsTo(fields: ["postID"])
19 content: String!
20}
21
22type Note @model {
23 id: ID!
24 postID: ID! @index(name: "byNote", sortKeyFields: ["content"])
25 post: Post! @belongsTo(fields: ["postID"])
26 content: String!
27}

If you only want to retrieve the comments, without the notes, create a GraphQLRequest for the Post with nested fields only containing the comment fields.

1extension GraphQLRequest {
2 static func getPostWithComments(byId id: String) -> GraphQLRequest<Post> {
3 let document = """
4 query getPost($id: ID!) {
5 getPost(id: $id) {
6 id
7 title
8 rating
9 status
10 comments {
11 items {
12 id
13 postID
14 content
15 }
16 }
17 }
18 }
19 """
20 return GraphQLRequest<Post>(
21 document: document,
22 variables: ["id": id],
23 responseType: Post.self,
24 decodePath: "getPost"
25 )
26 }
27}

Query with try await Amplify.API.query(request: .getPostWithComments(byId: "[POST_ID]")).

Combining multiple GraphQL operations in a single request

GraphQL allows you to run multiple GraphQL operations (queries/mutations) as part of a single network request from the client code. To perform multiple operations in a single request, you can place them within the same GraphQL document. For example, to retrieve a Post and a Todo:

1extension GraphQLRequest {
2 static func get(byPostId postId: String, todoId: String) -> GraphQLRequest<JSONValue> {
3 let document = """
4 query get($postId: ID!, $todoId: ID!) {
5 getPost(id: $postId) {
6 id
7 title
8 rating
9 }
10 getTodo(id: $todoId) {
11 id
12 name
13 }
14 }
15 """
16
17 return GraphQLRequest<JSONValue>(
18 document: document,
19 variables: [
20 "postId": postId,
21 "todoId": todoId
22 ],
23 responseType: JSONValue.self
24 )
25 }
26}

Notice here that JSONValue is used as the responseType. JSONValue is utility type that can be used to represent an arbitrary JSON response.

Once you have the response data in a JSONValue, you can access each object in the JSON structure by encoding it back to Data and decoding it to the expected Model.

1do {
2 let response = try await Amplify.API.query(request: .get(byPostId: "[POST_ID]", todoId: "[TODO_ID]"))
3 switch response {
4 case .success(let data):
5 if let todoJSON = data.value(at: "getTodo"),
6 let todoData = try? JSONEncoder().encode(todoJSON),
7 let todo = try? JSONDecoder().decode(Todo.self, from: todoData) {
8 print(todo)
9 }
10 if let postJSON = data.value(at: "getPost"),
11 let postData = try? JSONEncoder().encode(postJSON),
12 let post = try? JSONDecoder().decode(Post.self, from: postData) {
13 print(post)
14 }
15 case .failure(let errorResponse):
16 print("Response contained errors: \(errorResponse)")
17 }
18} catch let error as APIError {
19 print("Failed with error: \(error)")
20} catch {
21 print("Unexpected error: \(error)")
22}

If you have custom models or your Model has required fields that you have decided not to include in the response, you can create a Codable that conforms to the structure of the response data that you expect. From the previous example, the Codable would look like this

1struct PostAndTodoResponse: Codable {
2 public let getTodo: Todo
3 public let getPost: Post
4 struct Todo: Codable {
5 public let id: String
6 public var name: String
7 }
8 struct Post: Codable {
9 public let id: String
10 public var title: String
11 public var rating: Int
12 }
13}

Then use PostAndTodoResponse as the responseType of the GraphQLRequest instead of using JSONValue.

Combining multiple GraphQL requests on the client-side is different than server-side transaction support. To run multiple transactions as a batch operation refer to the Batch Put Custom Resolver example.

Adding Headers to Outgoing Requests

By default, the API plugin includes appropriate authorization headers on your outgoing requests. However, you may have an advanced use case where you wish to send additional request headers to AppSync.

If your API does not require any authorization or if you would like manipulate the request yourself, please refer to the Set authorization mode to NONE

To include custom headers in your outgoing requests, add an URLRequestInterceptor to the AWSAPIPlugin. Also specify the name of one of the APIs configured in your amplifyconfiguration.json file.

1struct CustomInterceptor: URLRequestInterceptor {
2 func intercept(_ request: URLRequest) throws -> URLRequest {
3 var request = request
4 request.setValue("headerValue", forHTTPHeaderField: "headerKey")
5 return request
6 }
7}
8let apiPlugin = try AWSAPIPlugin()
9try Amplify.addPlugin(apiPlugin)
10try Amplify.configure()
11try apiPlugin.add(interceptor: CustomInterceptor(), for: "yourApiName")