Created
October 19, 2023 17:43
-
-
Save rmosolgo/ee18246aed625d1f0d6b229613a331f0 to your computer and use it in GitHub Desktop.
Custom connection-like pagination system in GraphQL-Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "bundler/inline" | |
gemfile do | |
gem "graphql" | |
end | |
class Schema < GraphQL::Schema | |
# In your base field class, create a new field extension | |
# and configure your fields to use that one instead of GraphQL-Ruby's default. | |
# @see https://graphql-ruby.org/type_definitions/field_extensions.html | |
class BaseField < GraphQL::Schema::Field | |
# simplified from GraphQL::Schema::Field::ConnectionExtension | |
class CustomConnectionExtension < GraphQL::Schema::FieldExtension | |
def apply | |
field.argument :cursor, "String", required: false | |
field.argument :first, "Int", required: false | |
field.argument :last, "Int", required: false | |
end | |
# Remove pagination args before passing it to a user method | |
def resolve(object:, arguments:, context:) | |
next_args = arguments.dup | |
next_args.delete(:first) | |
next_args.delete(:last) | |
next_args.delete(:cursor) | |
yield(object, next_args, arguments) | |
end | |
def after_resolve(value:, object:, arguments:, context:, memo:) | |
# `memo` was the original `arguments` value from `resolve`. | |
# Normalize those arguments here for GraphQL-Ruby's connection objects. | |
connection_arguments = if memo[:first] | |
{ | |
first: memo[:first], | |
after: memo[:cursor], | |
} | |
else | |
{ | |
last: memo[:last], | |
before: memo[:cursor] | |
} | |
end | |
# Use GraphQL-Ruby's connection system to find a wrapper for `value` and apply it. | |
# For code that handles GraphQL-Batch Promises, see graphql/schema/field/connection_extension.rb. | |
context.namespace(:connections)[:all_wrappers] ||= context.schema.connections.all_wrappers | |
context.schema.connections.wrap(field, object.object, value, connection_arguments, context) | |
end | |
end | |
connection_extension(CustomConnectionExtension) | |
end | |
class BaseObject < GraphQL::Schema::Object | |
field_class(BaseField) | |
# Make a very simple connection-like base object | |
# and add a method for dynamically creating new types | |
# based on incoming object types. | |
class BaseConnection < BaseObject | |
def self.create_type(node_type) | |
Class.new(self) do | |
graphql_name("#{node_type.graphql_name}Connection") | |
field :nodes, [node_type] | |
field :next_cursor, String, method: :end_cursor | |
field :prev_cursor, String, method: :start_cursor | |
end | |
end | |
end | |
# Override GraphQL-Ruby's `.connection_type` helper | |
# with one that calls our own type builder. | |
def self.connection_type | |
@connection_type ||= BaseConnection.create_type(self) | |
end | |
end | |
class Book < BaseObject | |
field :title, String | |
end | |
class Query < BaseObject | |
field :books, Book.connection_type, null: false | |
def books | |
[ | |
{ title: "The Going to Bed Book" }, | |
{ title: "Jayber Crow" }, | |
{ title: "Lilith" }, | |
{ title: "American Farmstead Cheese" }, | |
{ title: "Ruby Under a Microscope" }, | |
] | |
end | |
end | |
query(Query) | |
end | |
puts Schema.to_definition | |
# type Book { | |
# title: String | |
# } | |
# type BookConnection { | |
# nextCursor: String | |
# nodes: [Book!] | |
# prevCursor: String | |
# } | |
# type Query { | |
# books(cursor: String, first: Int, last: Int): BookConnection! | |
# } | |
query_str = <<-GRAPHQL | |
query GetBooks($cursor: String) { | |
books(first: 2, cursor: $cursor) { | |
nodes { title } | |
nextCursor | |
prevCursor | |
} | |
} | |
GRAPHQL | |
res1 = Schema.execute(query_str) | |
pp res1.to_h | |
# {"data"=>{"books"=>{"nodes"=>[{"title"=>"The Going to Bed Book"}, {"title"=>"Jayber Crow"}], "nextCursor"=>"Mg", "prevCursor"=>"MQ"}}} | |
next_cursor = res1["data"]["books"]["nextCursor"] | |
res2 = Schema.execute(query_str, variables: { cursor: next_cursor }) | |
pp res2.to_h | |
# {"data"=>{"books"=>{"nodes"=>[{"title"=>"Lilith"}, {"title"=>"American Farmstead Cheese"}], "nextCursor"=>"NA", "prevCursor"=>"Mw"}}} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment