aws-amplify / amplify-android

The fastest and easiest way to use AWS from your Android app.
https://docs.amplify.aws/lib/q/platform/android/
Apache License 2.0
250 stars 117 forks source link

Custom Graph QL queries have no pagination support #1361

Closed ksgangadharan closed 3 years ago

ksgangadharan commented 3 years ago

Before opening, please confirm:

Language and Async Model

Kotlin

Amplify Categories

GraphQL API

Gradle script dependencies

implementation 'com.amplifyframework:aws-api:1.17.6'

Environment information

Build time: 2020-11-16 17:09:24 UTC Revision: 2972ff02f3210d2ceed2f1ea880f026acfbab5c0 Kotlin: 1.3.72 Groovy: 2.5.12 Ant: Apache Ant(TM) version 1.10.8 compiled on May 10 2020 JVM: 1.8.0_231 (Oracle Corporation 25.231-b11) OS: Mac OS X 10.16 x86_64

Please include any relevant guides or documentation you're referencing

No response

Describe the bug

We have to invoke a custom list graphQL query for fetching records based on certain implementation specific criteria. This included sorting of the records and use of certain custom parameters that are not supported by AppSyncGraphQLRequest.

As a solution, we wrote our own graphQL query by extending the GraphQLRequest class. The query runs fine and we are able to get the results. However the pagination does not work. The reason for the pagination not working is the following piece of code in GsonGraphQLResponseFactory.java

private PaginatedResult<Object> buildPaginatedResult(Iterable<Object> items, JsonElement nextTokenElement) {
            GraphQLRequest<PaginatedResult<Object>> requestForNextPage = null;
            if (nextTokenElement.isJsonPrimitive()) {
                String nextToken = nextTokenElement.getAsJsonPrimitive().getAsString();
                try {
                    if (request instanceof AppSyncGraphQLRequest) {
                        requestForNextPage = ((AppSyncGraphQLRequest<R>) request).newBuilder()
                                .variable(NEXT_TOKEN_KEY, "String", nextToken)
                                .build();
                    }
                } catch (AmplifyException exception) {
                    throw new JsonParseException(
                        "Failed to create requestForNextPage with nextToken variable",
                        exception
                    );
                }
            }
            return new PaginatedResult<>(items, requestForNextPage);
        }

The code here assumes that the GraphQL request is a type of AppSyncGraphQLRequest. This will fail for the custom query. We are unable to get a workaround for this issue as: 1) The AppSyncGraphQLRequest is final, so we cannot create a custom extension 2) The nextToken string is not available in the GraphQL Response for us to handle this independently

The issue can be resolved in 2 possible ways (without causing any extensive changes)

Solution 1: Add a function in GraphQLRequest class for handling pagination

    /**
     * Returns a new query object to be used for the next set of paginated results
     * @param nextToken token received in the previous query
     * @return the GraphQL request to be used for fetching the next page of results
     */
    public GraphQLRequest<PaginatedResult<Object>> getNextPaginatedQuery(String nextToken) {
        return null;
    }

And modify the GsonGraphQLResponseFactory.java pagination function as follows:

private PaginatedResult<Object> buildPaginatedResult(Iterable<Object> items, JsonElement nextTokenElement) {
            GraphQLRequest<PaginatedResult<Object>> requestForNextPage = null;
            if (nextTokenElement.isJsonPrimitive()) {
                String nextToken = nextTokenElement.getAsJsonPrimitive().getAsString();
                try {
                    if (request instanceof AppSyncGraphQLRequest) {
                        requestForNextPage = ((AppSyncGraphQLRequest<R>) request).newBuilder()
                                .variable(NEXT_TOKEN_KEY, "String", nextToken)
                                .build();
                    }
                    else if (request != null) {
                        requestForNextPage = request.getNextPaginatedQuery(nextToken);
                    }
                } catch (AmplifyException exception) {
                    throw new JsonParseException(
                        "Failed to create requestForNextPage with nextToken variable",
                        exception
                    );
                }
            }
            return new PaginatedResult<>(items, requestForNextPage);
        }

Solution 2: Make the nextToken available in the GraphQLResponse. This will enable the implementation to handle it as required

Reproduction steps (if applicable)

No response

Code Snippet

No response

Log output

No response

amplifyconfiguration.json

No response

GraphQL Schema

No response

Additional information and screenshots

No response

richardmcclellan commented 3 years ago

HI @ksgangadharan,

When using a custom GraphQL query, I'd suggest using a custom response type as well, instead of trying to make PaginatedResult work for your use case.

First, create some classes for the response type you are expecting:

class QueryList<T> {
     private Iterable<T> items;
     private String nextToken;

     public Iterable<T> getItems() { return items; }
     public String getNextToken() { return nextToken; }
}
class QueryResponse<T> {
      private QueryList<T> listTodos;

      public QueryList<T> getTodos() { return listTodos; }
}

Then, build your request like this, and specify the response type as QueryResponse<Todo>:

private GraphQLRequest<Todo> getListTodosRequest() {
    String document = "query listTodos { "
        + "listTodos { "
            + "items {
                + "id "
                + "name "
            + "}"
        + "}"
    + "}";
    return new SimpleGraphQLRequest<>(
            document, 
            Collections.emptyMap(),
            TypeMaker.getParameterizedType(QueryResponse.class, Todo.class) 
            new GsonVariablesSerializer());
}

Finally, you can access the list of Todo objects via:

response.getTodos().getItems();

and the nextToken via:

`response.getTodos().getNextToken()`
ksgangadharan commented 3 years ago

Thanks @richardmcclellan, that worked

I wish the classes were abstracted at a higher level instead of having dependencies on PaginatedResult and AppSyncGraphQLRequest. The above solution is a roundabout way and will need a custom response class for each new custom query. But yes it works.

Also anyone using this solution, be aware to update the pro guard files (i.e. if you using pro guarding).