RStankov / SearchObjectGraphQL

GraphQL plugin for SearchObject gem
MIT License
159 stars 25 forks source link

creating an after resolve action/callback to use with pagination #3

Closed woahstorkdork closed 6 years ago

woahstorkdork commented 6 years ago

So fundamentally my main question is:

  1. How can I implement an after execution/resolve callback for SearchObjectGraphQL?

My use case for this is pagination, hence the pagination-flavored question. I'd also welcome any other alternatives/suggestions for achieving the end behavior.

Currently Implemented: I set up an option within the search object resolver for pagination... option( :paginate, type: Types::PaginationInput, with: :pagination, default: { first: 10, skip: 0} ) and I have simple limit offset method to handle this...

def pagination(scope, value)
    scope.limit(value["first"]).offset(value["skip"])
  end

Note: I'm currrently returning a list of Products type !types[Types::ProductType]

The Problem The only problem with that is that through this implementation the client doesn't know what is the total number of records and so it makes it difficult to create any meaningful pagination ui/controls.

Ideally, I'd like to return a records count and a pages account as well. I noticed that your base SearchObject has a kaminari plugin and that it seems to also work with the graphql plugin. I mention this because I'd like to use kaminari's helpers to achieve this functionality.

The Ideal solution or so I think I'd like to create a custom type to return a paginated list of Products, along with the pagination metadata. Something along the lines of

  type do
    name 'PaginatedProducts'
    field :products, types[Types::ProductType]
        field :totalRecordsCount, types.Int
        field :totalPagesCount, types.Int
  end

And to achieve this, I think it'd be ideal to set up some kind of after_resolve callback that gets called at the end of processing scope through all of the SearchObject's options to create/return an OpenStruct following the custom type's format:

OpenStruct.new(
  products: scope.page(1),per(10)
  totalRecordsCount: scope.count
  totalPagesCount: scope.total_pages
)

Any and all help with this would be GREATLY appreciated!!!! It'll continue trying different things in the mean time.

Thank you so much for your awesome work @RStankov !

RStankov commented 6 years ago

Hi,

This is a bit tricky. I would suggest todo something like the following:

module Resolvers
  class PostSearch < Resolvers::BaseSearchResolver
    # you are correct to have a special type for grouping the result
    type do 
      name 'PaginatedPosts'

      field :posts, !types[!Types::PostType], property: :results
      field :postsCount, !types.Int, property: :count
      field :pagesCount, !types.Int, property: :pages_count
    end

    # overwrite this, so instead of results, it returns SearchObject
    def self.call(object, args, context)
      new(filters: args.to_h, object: object, context: context)
    end

    # you can define custom methods here
    def pages_count
      # ... calculate pages
    end

    # Rest of the code from:
    # https://github.com/RStankov/SearchObjectGraphQL/blob/master/example/app/graphql/resolvers/post_search.rb
  end
end

I hope this helps đŸ™Œ

woahstorkdork commented 6 years ago

I'll give it a shot. Thank you so much !

RStankov commented 6 years ago

You are welcome :)

woahstorkdork commented 6 years ago

For anyone that may be interested in a way to solve pagination, I ended up implementing a Resolvers::ListResolverBasethat all of the SearchObjects inherit from.

Here is the ListResolverBase:

require 'search_object/plugin/graphql'

class Resolvers::ListResolverBase
  include SearchObject.module(:graphql)
  include SearchObject.module(:kaminari)

  option( :paginate,  type: Types::PaginationInput, with: :pagination, default: {page: 1, per: 10} )

  def self.call(object, args, context)
        search_object = new( filters: args.to_h, object:  object, context: context)
    results, paginate = search_object.results, args["paginate"]

    return self.paginate_results(results, paginate)
  end

private
  def self.paginate_results(results, paginate)
    records = results.page(paginate["page"])
                       .per(paginate["per"])
    OpenStruct.new(
      data: records,
      recordsCount: records.total_count,
      pagesCount: records.total_pages,
      currentPage: records.current_page
    )
  end

  def pagination(scope, value)
  end

end

and I simply define a custom type in the respective resolver inhering from this class:

  type do 
    name 'PaginatedProducts'
    field :data, !types[!Types::ProductType]
    field :recordsCount, types.Int
    field :pagesCount, types.Int
    field :currentPage, types.Int
  end

voila, all SearchObjects inheriting from Resolvers::ListResolverBase and with a defined custom type now have pagination !

Thanks for your help in solving this @RStankov !

PS. Side question. I felt it was a little hacky adding an empty paginate option/method in order to whitelist those params. I tried adding an argument argument :paginate, type: Types::PaginationInput but it threw an error. Is there a way to define an argument in SearchObject without a corresponding option method?