amplify-swift/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.md

9.1 KiB

Model-based GraphQL API queries

APIPlugin at its core provides functionality to make requests to a GraphQL service. There are key developer use cases that can be achieved when using Amplify's Model types generated from Amplify CLI after provisioning a GraphQL AppSync service through the amplify add api command. Use-case centric APIs (Amplify.API.query, Amplify.API.mutate, and Amplify.API.subscribe), coupled with the GraphQLRequest builders (.get, .create, etc), provide a simple way to perform operations on a model or to retrieve instances of the model. The following goes over the algorithms used to perform successful operations for retrieving a model, a model with associations, and a list of models.

  1. As a developer, I want to retrieve a simple model object.

GraphQL Model

type SimpleModel @model {
  id: ID
}

Swift code generated by Amplify

struct SimpleModel: Model {
  let id: String
}

App code

Amplify.API.query(request: .get(SimpleModel.self, byId: id))
  • A GraphQLRequest is created from .get(SimpleModel.self, byId: id) containing the request payload such as the document, variables, and SimpleModel response type.

GraphQL document generated by GraphQLRequest.get(ModelType:id)

query getSimpleModel($input: GetSimpleModelInput!) {
  getSimpleModel(input: $input) {
    id
  }
}

GraphQL variables

"input": {
  "id": "[UNIQUE-ID]"
}
  • The document contains the selection set that indicates which fields of the model are returned in the response. The response type lets the system decode the raw GraphQL response data into a model instance.
  • AWSGraphQLOperation serializes the request and performs the network call to the AppSync service.
  • Upon getting a successful response from the service, GraphQLResponseDecoder extracts the data portion of the response.
  • The model instance data is decoded to the response type using the JSONDecoder to return a model instance.
  1. As a developer, I want to retrieve a list of simple model objects.

App code

Amplify.API.query(request: .list(SimpleModel.self))
  • .list(SimpleModel.self) will create a GraphQLRequest with selection set containing "items" and "nextToken", response type List<SimpleModel>, and variables containing the limit of 1000.
  • GraphQLResponseDecoder checks if the response type conforms to ModelListMarker. If so, it encapsulates the original request and response using an AppSyncListPayload, and decodes the AppSyncListPayload to the List type.
  • List's custom decoder logic will detect the AppSyncListPayload by first finding registered decoders in the ModelListDecoderRegistry. A registered decoder is used to decode the response into a ModelListProvider before instantiating the list instance.
  • The AppSyncListDecoder is returned from the ModelListDecoderRegistry. At config time, the AWSAPIPlugin registers AppSyncListDecoder with ModelListDecoderRegistry to provide runtime decoding functionality.
  • AppSyncListDecoder checks that the data can be successfully decoded to an AppSyncListPayload, extracts the original request variables and response, vends a AppSyncListProvider, and instanatites a List with the list provider.

The developer now has List of models, and can check if there are subsequent pages to retrieve by calling List.getNextPage(). Internally, the AppSyncListProvider looks at the response payload's nextToken field, and can retrieve the next page or results with the same limit and filter as the original request.

if models.hasNextPage() {
  models.getNextPage()
}
  1. As a developer, I want to retrieve a model that contains associations to other models.

GraphQL Model

# A `Post` model contains an association to the `Comment` as an "has many" array association.
type Post @model {
  id: ID
  comments: [Comment] @connection(keyName: "byPost", fields: ["id"])
}

# The `Comment` belongs to a `Post.
type Comment @model @key(name: "byPost", fields: ["postID"]) {
  id: ID!
  postID: ID!
  post: Post @connection(fields: ["postID"])
}

Swift code generated by Amplify

struct Post: Model {
  let id: String
  let comments: List<Comment>?
}
struct Comment: Model {
  let id: String
  let post: Post?
}

App code

Amplify.API.query(request: .get(Post.self, byId: id))
  • A normal GraphQL selection set could contain multiple levels of data (e.g., the first level being the Post, the second level being the list of Comments, and so on). However, the plugin detects this and only creates a selection set containing the first "level" of results. This provides a scalable approach to retrieve models with a "not loaded" association, and allows the developer to lazy load the associations later. Without this, a response could potentially have to return the entire object graph in order to maintain referential integrity.

A comparison between the first level of results and the second level of results

First level of results

query getPost($input: GetPostInput!) {
  getPost(input: $input) {
    id
  }
}

Second level of results

query getPost($input: GetPostInput!) {
  getPost(input: $input) {
    id
    comments {
      items {
        id
      }
      nextToken
    }
  }
}
  • GraphQLResponseDecoder decodes the post response data to a mutable JSONValue object as an intermediate step.
{
  "id": "[POST_ID]"
}
  • GraphQLResponseDecoder analyzes the object's model schema. It stores association data (e.g., post.id and "post" field name) as an AppSyncModelMetadata in the instance JSONValue's "comments" key.
{
  "id": "[POST_ID]",
  "comments": {
    "appSyncAssociatedId": "[POST_ID]",
    "appSyncAssociatedField": "post"
  }
}
  • GraphQLResponseDecoder serializes object and decodes it to a Post instance as described above. The Post model-specific decoder instantiates scalar fields normally, while decoding the comments field into a List<Comment> that delegates its logic to a AppSyncListDecoder.
  • AppSyncListDecoder checks that the data can be successfully decoded to an AppSyncModelMetadata and stores this information in an AppSyncListProvider when instantiating a "not loaded" list with association data.

The developer now has the Post instance and can either explicitly load the comments or lazy load the comments upon accessing iterator methods

Explicit load

if let comments = post.comments {
  comments.fetch { result in
      switch result {
      case .success:
          print("fetch completed, list data is now loaded into memory")
      case .failure(let error):
          print("Could not fetch posts \(error)")
      }
  }
}

Implicit load

foreach comment in post.comments {
   /// comments is loaded before the first element is returned.
}
  • The comments are implicitly loaded upon access by performing a query using the association data stored in the AppSyncListProvider. The plugin queries AppSync for Comments where comment.postId == post.id, as defined by the association in the model schema. It then decodes the query response into a List object as shown above, and returns that to the caller that is performing the access.
Amplify.API.query(request: .list(Comment.self, limit: 100, where: Comment.keys.post == post.id))
  1. As a developer, I can customize my request to retrieve multiple levels of data at once
let document = """
  query getBlog($id: ID!) {
    getBlog(id: $id) {
      id
      post {
        items {
          id
          comments {
            items {
              id
            }
            nextToken
          }
        }
        nextToken
      }
    }
  }
  """
  • GraphQLResponseDecoder will not augment the response data at "post" and "comments" with association data, since the response data already contains the payload to be decoded to the List.

Response data

{
  "id": "[BLOG_ID]",
  "post": {
    "items": [
      {
        "id": "[POST_ID]",
        "comments": {
          "items": [
            {
              "id": "[COMMENT_ID]"
            }
          ],
          "nextToken": "nextToken"
        }
      }
    ],
    "nextToken": "nextToken"
  }
}
  • AppSyncListDecoder checks that the data can be successfully decoded to AppSyncListResponse and instantaites a loaded list of post and list of comments respectively in the chain of decoders.

This is an advanced use case, and has some caveats regarding the subsequent API calls performed from the List:

  • hasNextPage() relies on the selection set to contain nextToken, so if this is excluded from the selection set, then hasNextPage() will always return false.
  • getNextPage(completion:) does not retrieve the next page according to the associated parent since association data was never added to the List provider. In this flow the returned data represents more of a snapshot, and assumes that the developer understands what they are trying to achieve with the customization. Alternately, developers can go to the full extent of modifying the response type as well to AppSyncListResponse to control exactly what the AppSync service returns in the response.