Skip to content

Instantly share code, notes, and snippets.

@rmosolgo
Created October 19, 2023 17:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rmosolgo/ee18246aed625d1f0d6b229613a331f0 to your computer and use it in GitHub Desktop.
Save rmosolgo/ee18246aed625d1f0d6b229613a331f0 to your computer and use it in GitHub Desktop.
Custom connection-like pagination system in GraphQL-Ruby
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