## 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** ```graphql type SimpleModel @model { id: ID } ``` **Swift code generated by Amplify** ```swift struct SimpleModel: Model { let id: String } ``` **App code** ```swift 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** ```json "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. 2. As a developer, I want to retrieve a list of simple model objects. **App code** ```swift Amplify.API.query(request: .list(SimpleModel.self)) ``` - `.list(SimpleModel.self)` will create a `GraphQLRequest` with selection set containing "items" and "nextToken", response type `List`, 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. ```swift if models.hasNextPage() { models.getNextPage() } ``` 3. As a developer, I want to retrieve a model that contains associations to other models. **GraphQL Model** ```graphql # 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** ```swift struct Post: Model { let id: String let comments: List? } struct Comment: Model { let id: String let post: Post? } ``` **App code** ```swift 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. ```json { "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. ```json { "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` 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** ```swift 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** ```swift 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. ```swift Amplify.API.query(request: .list(Comment.self, limit: 100, where: Comment.keys.post == post.id)) ``` 4. As a developer, I can customize my request to retrieve multiple levels of data at once ```swift 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** ```json { "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.