spring-bootgraphqlspring-graphql

How to sort @GraphQlRepository paginated results


I’m currently evaluating the potential use of Spring Boot GraphQL for future web projects. So far, I’ve had positive experiences with filtering, pagination, and selects/joins. However, I haven’t found a way to apply sorting when using pagination.

Here's the Repository

@NoRepositoryBean
public interface BaseRepository<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T>, QueryByExampleExecutor<T>
{
}

@GraphQlRepository
public interface UserRepository extends BaseRepository<User, Long>
{
}

Here's the schema.graphqls

type Query {
    users(
        first: Int, 
        after: String, 
        last: Int, 
        before: String, 
        user: UserInput
    ): UserConnection
    user(id: ID!): User
}

input UserInput {
    username: String
    firstname: String
    lastname: String
}

type User {
    id: ID!
    username: String!
    firstname: String
    lastname: String
    roles: [Role!]
}

type Role {
 id: ID!
 name: String!
}

And here's the query

query {
  users(
    user: {
      firstname: "Max"
    }, 
    first: 20,
  ) {
    edges {
      node {
        id
        username
        firstname
        lastname
        roles {
          id
          name
        }
      }
    }
    pageInfo {
      hasPreviousPage
      hasNextPage
      startCursor
      endCursor
    }
  }
}

I'm looking for something like this:

query {
      users(
        user: {
          firstname: "Max"
        }, 
        first: 20,
        orderBy: {
          lastname: DESC
        }
      ) {
        ...
      }
    }

Thank you!


Solution

  • My solution is to register a custom data fetcher where I can access the query and add the sort. There a more than one possible way to achieve this. For me the most convinient way was to just implement QueryByExampleBuilderCustomizer and replace the Builder in customize

    @NoRepositoryBean
    public interface BaseRepository<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T>, QueryByExampleExecutor<T>, QueryByExampleBuilderCustomizer<T>
    {
        @Override
        default Builder<T, ?> customize(Builder<T, ?> builder)
        {
            return CustomBuilder.create(this);
        }
        
    }
    

    To reuse spring boot stuff this has to be in package org.springframework.graphql.data.query. I know using internal stuff is not a good idea in the long run, but i'm hoping spring boot will add sorting to pagination in future releases since it is commonly needed although the specification does not explicitly specify it but definetly does not forbid it.

    public class CustomBuilder<T, R> extends QueryByExampleDataFetcher.Builder<T, R>
    {
        public static <T, R> CustomBuilder<T, R> create(QueryByExampleExecutor<T> executor) {
            Class<R> domainType =  RepositoryUtils.getDomainType(executor);
            return new CustomBuilder<>(executor, domainType);
        }
        
        
        
        private final QueryByExampleExecutor<T> executor;
    
        private final TypeInformation<T> domainType;
    
        private final Class<R> resultType;
    
        
        
        @SuppressWarnings("unchecked")
        CustomBuilder(QueryByExampleExecutor<T> executor, Class<R> domainType)
        {
            super(executor, domainType);
            
            this.executor = executor;
            this.domainType = TypeInformation.of((Class<T>) domainType);
            this.resultType = domainType;
    
        }
        
        
        
        @Override
        public DataFetcher<Iterable<R>> scrollable() {
            return new SortableScrollableEntityFetcher<>(
                    this.executor, this.domainType, this.resultType,
                    RepositoryUtils.defaultCursorStrategy(),
                    RepositoryUtils.defaultScrollCount(),
                    RepositoryUtils.defaultScrollPosition(),
                    Sort.unsorted());
        }
        
        
        
        static class SortableScrollableEntityFetcher<T, R> 
                extends QueryByExampleDataFetcher<T> implements SelfDescribingDataFetcher<Iterable<R>> {
            
            private final QueryByExampleExecutor<T> executor;
    
            private final Class<R> resultType;
    
            private final Sort sort;
            
            private final CursorStrategy<ScrollPosition> cursorStrategy;
    
            private final int defaultCount;
    
            private final Function<Boolean, ScrollPosition> defaultPosition;
    
            private final ResolvableType scrollableResultType;
    
            SortableScrollableEntityFetcher(
                    QueryByExampleExecutor<T> executor, 
                    TypeInformation<T> domainType,
                    Class<R> resultType, 
                    CursorStrategy<ScrollPosition> cursorStrategy,
                    int defaultCount,
                    Function<Boolean, ScrollPosition> defaultPosition,
                    Sort sort
            ) {
                super(domainType);
                
                this.executor = executor;
                this.resultType = resultType;
                this.sort = sort;
                this.cursorStrategy = cursorStrategy;
                this.defaultCount = defaultCount;
                this.defaultPosition = defaultPosition;
                this.scrollableResultType = ResolvableType.forClassWithGenerics(Window.class, resultType);
            }
            
            @Override
            public ResolvableType getReturnType() {
                return ResolvableType.forClassWithGenerics(Iterable.class, this.scrollableResultType);
            }
    
            @Override
            @SuppressWarnings("unchecked")
            public Iterable<R> get(DataFetchingEnvironment env) throws BindException {
                return this.executor.findBy(buildExample(env), (query) -> {
                    FluentQuery.FetchableFluentQuery<R> queryToUse = (FluentQuery.FetchableFluentQuery<R>) query;
    
                    if (this.sort.isSorted()) {
                        queryToUse = queryToUse.sortBy(this.sort);
                    }
    
                    if (requiresProjection(this.resultType)) {
                        queryToUse = queryToUse.as(this.resultType);
                    }
                    else {
                        queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType));
                    }
    
                    return getResult(queryToUse, env);
                });
            }
    
            protected Iterable<R> getResult(FluentQuery.FetchableFluentQuery<R> queryToUse, DataFetchingEnvironment env) {
                ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy);
                int count = range.count().orElse(this.defaultCount);
                ScrollPosition position = (range.position().isPresent() ?
                        range.position().get() : this.defaultPosition.apply(range.forward()));
                
                Sort sort = fromJson(env.getArgument("sort"));
                
                return queryToUse.limit(count).sortBy(sort).scroll(position);
            }
            
            @SuppressWarnings("unchecked")
            private static Sort fromJson(Object json) {
                ObjectMapper mapper = new ObjectMapper();
                Map<String, Object> map = mapper.convertValue(json, Map.class);
                Object ordersObj = map.get("orders");
    
                if (!(ordersObj instanceof List<?> orders)) {
                    return Sort.unsorted();
                }
    
                List<Sort.Order> orderList = orders.stream()
                    .filter(o -> o instanceof Map<?, ?>)
                    .map(o -> (Map<String, Object>) o)
                    .map(entry -> new Sort.Order(
                        Sort.Direction.fromOptionalString((String) entry.getOrDefault("direction", "ASC")).orElse(Sort.Direction.ASC),
                        (String) entry.get("property")
                    ))
                    .toList();
    
                return Sort.by(orderList);
            }
            
        }
    
    }