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.
- 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, andSimpleModel
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.
- 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 aGraphQLRequest
with selection set containing "items" and "nextToken", response typeList<SimpleModel>
, and variables containing thelimit
of 1000.GraphQLResponseDecoder
checks if the response type conforms toModelListMarker
. If so, it encapsulates the original request and response using anAppSyncListPayload
, and decodes theAppSyncListPayload
to theList
type.List
's custom decoder logic will detect theAppSyncListPayload
by first finding registered decoders in theModelListDecoderRegistry
. A registered decoder is used to decode the response into aModelListProvider
before instantiating the list instance.- The
AppSyncListDecoder
is returned from theModelListDecoderRegistry
. At config time, theAWSAPIPlugin
registersAppSyncListDecoder
withModelListDecoderRegistry
to provide runtime decoding functionality. AppSyncListDecoder
checks that the data can be successfully decoded to anAppSyncListPayload
, extracts the original request variables and response, vends aAppSyncListProvider
, and instanatites aList
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()
}
- 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 ofComments
, 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 thepost
response data to a mutableJSONValue
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 anAppSyncModelMetadata
in the instanceJSONValue
's"comments"
key.
{
"id": "[POST_ID]",
"comments": {
"appSyncAssociatedId": "[POST_ID]",
"appSyncAssociatedField": "post"
}
}
GraphQLResponseDecoder
serializes object and decodes it to aPost
instance as described above. ThePost
model-specific decoder instantiates scalar fields normally, while decoding thecomments
field into aList<Comment>
that delegates its logic to aAppSyncListDecoder
.AppSyncListDecoder
checks that the data can be successfully decoded to anAppSyncModelMetadata
and stores this information in anAppSyncListProvider
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 forComments
wherecomment.postId == post.id
, as defined by the association in the model schema. It then decodes the query response into aList
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))
- 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 theList
.
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 toAppSyncListResponse
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 containnextToken
, so if this is excluded from the selection set, thenhasNextPage()
will always returnfalse
.getNextPage(completion:)
does not retrieve the next page according to the associated parent since association data was never added to theList
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 toAppSyncListResponse
to control exactly what the AppSync service returns in the response.