amplify-swift icon indicating copy to clipboard operation
amplify-swift copied to clipboard

Amplify.API - custom query pagination issue

Open rcastrovivar opened this issue 4 years ago • 3 comments

Describe the bug

When using custom queries the ".getNextPage" method returned doesn't use the custom query to fetch next pages, it instead uses the default query but since this default query is not compatible with the custom query NextToken the call fails schema:

type SentMessage @model @key(name: "byChannelId", fields: ["channelid", "createdAt"], queryField: "messagesByChanneId") {
  id: ID!
  senderid: String!
  recieverid: String!
  channelid: String!
  createdAt: AWSDateTime!
  content: String!
}

custom query:

    static func getMessagesByChannelId(_ channelId: String, pageSize: Int) -> GraphQLRequest<List<SentMessage>> {
        let operationName = "messagesByChanneId"
        let document = """
            query messagesByChanneId($channelid: String, $filter: ModelSentMessageFilterInput, $sortDirection: ModelSortDirection, $limit: Int) {
                messagesByChanneId (channelid: $channelid, filter: $filter, sortDirection: $sortDirection, limit: $limit) {
                    items {
                        id
                        channelid
                        content
                        createdAt
                        recieverid
                        senderid
                        __typename
                    }
                nextToken
                }
            }
            """
        return GraphQLRequest<List<SentMessage>>(
            document: document,
            variables: [
                "channelid": channelId,
                "sortDirection": "DESC",
                "limit": pageSize
            ],
            responseType: List<SentMessage>.self,
            decodePath: operationName)
    }

initial API call:

{
    "query": "query messagesByChanneId($channelid: String, $filter: ModelSentMessageFilterInput, $sortDirection: ModelSortDirection, $limit: Int) {\n    messagesByChanneId (channelid: $channelid, filter: $filter, sortDirection: $sortDirection, limit: $limit) {\n        items {\n            id\n            channelid\n            content\n            createdAt\n            recieverid\n            senderid\n            __typename\n        }\n    nextToken\n    }\n}",
    "variables": {
        "channelid": "eab3c25c-ecaf-4886-9c50-1e69fa1e7179",
        "limit": 10,
        "sortDirection": "DESC"
    }
}

getNextPage API call:

{
    "query": "query ListSentMessages($limit: Int, $nextToken: String) {\n  listSentMessages(limit: $limit, nextToken: $nextToken) {\n    items {\n      id\n      channelid\n      content\n      createdAt\n      recieverid\n      senderid\n      __typename\n    }\n    nextToken\n  }\n}",
    "variables": {
        "limit": 10,
        "nextToken": "eyJ2ZXJzaW9uIjoyLCJ0b2tlbiI6IkFRSUNBSGcxN1ltYUNNTENHbXlQNEt5OGRnNHFZVjZoTDVZemdnalZUZnRub2M5U2t3Rm00MitIcjI1WXE2THhZQTg4ZHJFa0FBQURvVENDQTUwR0NTcUdTSWIzRFFFSEJxQ0NBNDR3Z2dPS0FnRUFNSUlEZ3dZSktvWklodmNOQVFjQk1CNEdDV0NHU0FGbEF3UUJMakFSQkF4R0dFK2RMQXdCL00wK01hb0NBUkNBZ2dOVWJNRWtCSGpwNU5KTERodzVzQ1BWbm13eWx4ODczUnQ1bHFnVmtncWVjTjJrRU1xUHU0WFpsZ0pMZGJQNWtPa3FlUlZkYXl3MjdUaDAvcC9uUnpyYURISDQ5NEc1a0FqTGtFd0hQcXhWUVNHbGV6VXNVUStCb1pCeGJUQUJIaFRvNUFnVnNZNTNVUGUxdFpucmJVZEFhOVptMmFpV0NTMXBOQmxMYU5jSXRFNWpHUEhMWitJeStRQldLM0ZyNzcvbDdqZ3M4MGFzVkhQMDRlUFQ3ME1GVzRyQlI4RjJYNG40T3ozdHBiNk5SYy9uYTVzRHQ3bSttdXNvaDdqWk9kZXhtUVFUdGZRMDRwRjlVNzh0alRudVBnRHVPbWx2MlQ3M3ZEY0VISER3R0ZlRm9SR2p2QzcraXdWM0w2SmZzamtUaTZMNUVuTUVITWF5RVZmV0xiYXRoOWxUZHdxMUJ4RG1JK3JrVWJOU3c2ck5rUXJ3SjJmUlFYTkU2YjE1T1Z3REltaTlITU9LUFl1enFZcWxVcWtLRlVOSFdnTVU1R0xFM1VqQk5qVDQrUWNLNldKMlA5eVpwdno3T0gxdVFnc3o0SkwrU0liSEVDdXcyMW5pNFU5ay9XVVpRV0RHcmoxTUU4TTNpV1gxOFlJL3pqL1RjOTNSaHJWTnRhN3RXTHJ2VjhtMTJaT1BBYmZrbkN0WTVBMHBuNm53TFUySFhYQTlPRHl6bUlsaitZeE9MK1R6SWxydXhUN29BTDdaQzBLMlNUQXJaWjhqeUs4UDdGYzJNQVgwd25wRXNIbDRvdTZQdVdVNzJoQmF5aytvRFVRdjJhdzE5ZjcxTDd3am5mczBPN1NFaWZBUmdDR1VPZHdkemhoOUd1SUdRelBxTkZFaFVIWU14Y1pBRFNHSC9mQW1YK3JiVEtDcE4xWGdCMnRocVNrQVBsYzA5NnNxODl0TzRsbFZBUFdYRE1uOHlhVzBubkVJWklvcVJLYTVrZmhMTWFpeWZHdUQrQWNyTFB5d1NJTXd5cDlrTy9rdGZCTkRVL2UrSG5WUTllT1ZaTitNWlBmU2RPR2dyWEVoK0FMMFVXTllVdnRBSUtpSlU2VE9yZTlYc2xub0w4cE9iS1BTYUJYbDd6WmFaZnVGaTRncjJoL3JLTyt5SkNybmRSVFJ5N205VGRRdXdzY0ExVnU1R0p2QWluM0dmek1oOUZvemFwUFdoWG9jdzQzb1NWUUFaK055K3l6ekNzMU1RUVNRVlJqUWpZZkdwSVQvT2Npdi8xOHIvazNOeGp1djVqQjlhQ1ZRSFA0eFZXNkhlNTVKZjhvZ3doeVo5VFZlKzE2cy9xWUcwZitKUGliQkUybEdCdkJweUhOMVBQTXBlZEhXaXM2eFdCOTdrMmNxVnQ5WFR1VjY5NE9SVW4rY0FiWThnM01sd0RFTzV6RVgwOUtuQmw3TDUrVkZ6OEJUdUFjM3dETzZ1d3Qwa3F3VVJ5Vk4vQjFWWTc0Mm1nOHZLdDY1In0="
    }
}

and since the Next token used is not compatible with the query used to get next page, the call fails:

{
    "data": {
        "listSentMessages": null
    },
    "errors": [
        {
            "data": null,
            "errorInfo": null,
            "errorType": "DynamoDB:DynamoDbException",
            "locations": [
                {
                    "column": 3,
                    "line": 2,
                    "sourceName": null
                }
            ],
            "message": "The provided starting key is invalid: The provided key element does not match the schema (Service: DynamoDb, Status Code: 400, Request ID: 0672R8LD612MASAK3UU4O0DN5RVV4KQNSO5AEMVJF66Q9ASUAAJG, Extended Request ID: null)",
            "path": [
                "listSentMessages"
            ]
        }
    ]
}

Steps To Reproduce

Steps to reproduce the behavior:
1. Use custom query to fetch records
2. when more pages are available a NextToken is returned and the method getNextPage is available, call getNextPage

Expected behavior

Amplify.API should be smart enough to reuse the custom query in the GetNextPage method allowing us to paginate while using custom queries

Amplify Framework Version

1.12

Amplify Categories

API

Dependency manager

Swift PM

Swift version

5.0

CLI version

4.5.0

Xcode version

12.3

Relevant log output

No response

Is this a regression? (i.e. was this working before a version upgrade)

No response

Device

iPhone 7

iOS Version

iOS 14

Specific to simulators

No response

Additional context

No response

rcastrovivar avatar Jul 23 '21 17:07 rcastrovivar

Hi @rcastrovivar, thanks for the detailed description. This currently doesn't work because the provider used to get the next page has an hardcoded GraphQLRequest that uses listQuery.

To get this to work, you can change the response type to a type that can be decoded to:

struct ListResponse<M: Model> {
   let nextToken: String
   let items: [M]
}

then use it in as your response type: responseType: ListResponse<SentMessage>.self. This way, you can pull the nextToken back out, and call your custom query again with the next token: getMessagesByChannelId(_ channelId: String, pageSize: Int, nextToken: String).

I understand this isn't ideal but it should unblock your use case. For List to support custom request, I believe it's difficult because re-using the original request means maniupulating the document (a free form string at that point) and variables to inject the nextToken appropriately, which is prone to error.

lawmicha avatar Sep 24 '21 14:09 lawmicha

i have tried your work around and it does work, thanks for your response.

rcastrovivar avatar Sep 24 '21 16:09 rcastrovivar

@rcastrovivar Thanks for getting back and confirming it is working for you- I'll mark this as a feature request to better support this use case.

Constructing custom queries go beyond just getting the correct document and variables for the GraphQL request, but also getting the correct response type to decode the response from AppSync. Amplify CLI codegenerates the models which contain the associated has-many connected models as a List<Model>, which can easily be used as the responseType in GraphQLRequest. We should investigate how we can better support the use case of creating using custom graphql requests and re-using the models that are generated by amplify codegen models.

We know that the List object has metadata that can be used to perform fetching the first page or getting the next page. we can provide escape hatches from this object to better leverage the metadata that's already present from the custom decoding logic done in the plugin. For example, can we simply expose the nextToken that is an AppSync specific data?

let appSyncList = list as? AppSyncList, let nextToken = appSyncList.nextToken {
     getMessagesByChannelId(nextToken)
}

This way, developer does not need to modify the responseType to their own custom type and reuse List.

Can we provide a way to provide overrides?

// let request = list.getNextRequest() // Without overrides
let request = list.getNextRequest().override(document, variables) // with overrides
Amplify.API.query(request)

lawmicha avatar Sep 24 '21 20:09 lawmicha