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:
let todo = Todo(name: "my first todo", description: "todo description")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.
{ "query": "mutation createTodo($input: CreateTodoInput!) { createTodo(input: $input) { id name description } }", "variables": "{ "input": { "id": "[UNIQUE-ID]", "name": "my first todo", "description": "todo description" } }}
The different parts of the document are described as follows
mutation
- the operation type to be performed, other operation types arequery
andsubscription
createTodo($input: CreateTodoInput!)
- the name and input of the operation.$input: CreateTodoInput!
- the input of typeCreateTodoInput!
referencing the variables containing JSON inputcreateTodo(input: $input)
- the mutation operation which takes a variable input from$input
- the selection set containing
id
,name
, anddescription
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.
query getTodo($id: ID!) { getTodo(id: $id) { id name }}
The response data will look like this
{ "data": { "getTodo": { "id": "111", "name": "my first todo" } }}
First, create your own GraphQLRequest
extension GraphQLRequest { static func getWithoutDescription(byId id: String) -> GraphQLRequest<Todo> { let operationName = "getTodo" let document = """ query getTodo($id: ID!) { \(operationName)(id: $id) { id name } } """ return GraphQLRequest<Todo>( document: document, variables: ["id": id], responseType: Todo.self, decodePath: operationName ) }}
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
Amplify.API.query(request: .getWithoutDescription(byId: "[UNIQUE_ID]")) { // handle result
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.
enum PostStatus { ACTIVE INACTIVE}
type Post @model { id: ID! title: String! rating: Int! status: PostStatus! comments: [Comment] @hasMany(indexName: "byPost", fields: ["id"]) notes: [Note] @hasMany(indexName: "byNote", fields: ["id"])}
type Comment @model { id: ID! postID: ID! @index(name: "byPost", sortKeyFields: ["content"]) post: Post! @belongsTo(fields: ["postID"]) content: String!}
type Note @model { id: ID! postID: ID! @index(name: "byNote", sortKeyFields: ["content"]) post: Post! @belongsTo(fields: ["postID"]) content: String!}
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.
extension GraphQLRequest { static func getPostWithComments(byId id: String) -> GraphQLRequest<Post> { let document = """ query getPost($id: ID!) { getPost(id: $id) { id title rating status comments { items { id postID content } } } } """ return GraphQLRequest<Post>( document: document, variables: ["id": id], responseType: Post.self, decodePath: "getPost" ) }}
Query with Amplify.API.query(request: .getPostWithComments(byId: "[POST_ID]"))
.
Combining Multiple Operations
When you want to perform more than one operation in a single request, you can place them within the same document. For example, to retrieve a Post and a Todo
extension GraphQLRequest { static func get(byPostId postId: String, todoId: String) -> GraphQLRequest<JSONValue> { let document = """ query get($postId: ID!, $todoId: ID!) { getPost(id: $postId) { id title rating } getTodo(id: $todoId) { id name } } """ return GraphQLRequest<JSONValue>(document: document, variables: ["postId": postId, "todoId": todoId], responseType: JSONValue.self) }}
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.
Amplify.API.query(request: .get(byPostId: "[POST_ID]", todoId: "[TODO_ID]")) { result in switch result { case .success(let response): switch response { case .success(let data): if let todoJSON = data.value(at: "getTodo"), let todoData = try? JSONEncoder().encode(todoJSON), let todo = try? JSONDecoder().decode(Todo.self, from: todoData) { print(todo) } if let postJSON = data.value(at: "getPost"), let postData = try? JSONEncoder().encode(postJSON), let post = try? JSONDecoder().decode(Post.self, from: postData) { print(post) } case .failure(let errorResponse): print("Response contained errors: \(errorResponse)") } case .failure(let apiError): print("Failed with error: \(apiError)") }}
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
struct PostAndTodoResponse: Codable { public let getTodo: Todo public let getPost: Post struct Todo: Codable { public let id: String public var name: String } struct Post: Codable { public let id: String public var title: String public var rating: Int }}
Then use PostAndTodoResponse
as the responseType
of the GraphQLRequest
instead of using JSONValue
.
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.
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.
struct CustomInterceptor: URLRequestInterceptor { func intercept(_ request: URLRequest) throws -> URLRequest { let nsUrlRequest = (request as NSURLRequest) guard let mutableRequest = nsUrlRequest.mutableCopy() as? NSMutableURLRequest else { throw APIError.unknown("Could not get mutable request", "") } mutableRequest.setValue("headerValue", forHTTPHeaderField: "headerKey") return mutableRequest as URLRequest }}let apiPlugin = try AWSAPIPlugin()try Amplify.addPlugin(apiPlugin)try Amplify.configure()try apiPlugin.add(interceptor: CustomInterceptor(), for: "yourApiName")