Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 160 additions & 137 deletions Gemfile.lock

Large diffs are not rendered by default.

34 changes: 13 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
# GraphQL::FancyLoader

FancyLoader (built on top of the [graphql-batch][graphql-batch] gem) efficiently batches queries
> [!NOTE]
> This is a fork of the original [hummingbird-me/graphql-fancy-loader](https://github.com/hummingbird-me/graphql-fancy-loader) that has been migrated to use graphql-ruby's built-in dataloader instead of graphql-batch. If you need the original graphql-batch implementation, please use the upstream repository.

FancyLoader (built on top of graphql-ruby's dataloader) efficiently batches queries
using postgres window functions to allow advanced features such as orders, limits, pagination, and
authorization scoping. Built on top of Arel, FancyLoader is highly extensible and capable of
handling complex sorts (including sorting based on a join) with minimal effort and high performance.

We use FancyLoader in production to power large swaths of the Kitsu GraphQL API.

[graphql-batch]: https://github.com/Shopify/graphql-batch

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'graphql-fancy_loader'
gem 'graphql-fancy_loader', github: 'metafy-gg/graphql-fancy-loader'
```

And then execute:
Expand All @@ -23,12 +24,6 @@ And then execute:
$ bundle install
```

Or install it yourself as:

```
$ gem install graphql-fancy_loader
```

## Basic Usage

### Defining a Loader
Expand All @@ -54,7 +49,7 @@ end

This loader class provides two primary methods: `.sort_argument` and `.connection_for`.
`.sort_argument` gives you a convenient auto-generated sort field which allows for multiple sorts.
`.connection_for` is a wrapper around graphql-batch which returns a connection. Pagination will be
`.connection_for` is a wrapper around graphql-ruby's dataloader which returns a connection. Pagination will be
automatically applied to this connection by the graphql gem, so you just need to pass in your other
options:

Expand All @@ -77,8 +72,7 @@ end

### Testing the Loader

Testing a FancyLoader is pretty much exactly the same as testing any other graphql-batch Loader
class:
Testing a FancyLoader works with graphql-ruby's dataloader pattern:

```ruby
RSpec.describe Loaders::PostsLoader do
Expand All @@ -90,13 +84,11 @@ RSpec.describe Loaders::PostsLoader do
let(:sort) { [{ on: :created_at, direction: :desc }] }

it 'loads all the posts for a user' do
posts = GraphQL::Batch.batch do
described_class.connection_for({
find_by: :user_id,
sort: sort,
context: context
}, user.id).nodes
end
posts = described_class.connection_for({
find_by: :user_id,
sort: sort,
context: context
}, user.id).nodes

expect(posts.count).to eq(user.posts.count)
end
Expand Down Expand Up @@ -208,7 +200,7 @@ will create a git tag for the version, push git commits and tags, and push the `
## Contributing

Bug reports and pull requests are welcome on GitHub at
https://github.com/hummingbird-me/graphql-fancy-loader.
https://github.com/metafy-gg/graphql-fancy-loader.

## License

Expand Down
35 changes: 17 additions & 18 deletions graphql-fancy_loader.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,34 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'graphql/fancy_loader/version'

Gem::Specification.new do |spec|
spec.name = 'graphql-fancy_loader'
spec.version = GraphQL::FancyLoader::VERSION
spec.authors = ['Toyhammered', 'Emma Lejeck']
spec.email = ['[email protected]']
spec.name = 'graphql-fancy_loader'
spec.version = GraphQL::FancyLoader::VERSION
spec.authors = ['Toyhammered', 'Emma Lejeck', 'Thomas McNiven']
spec.email = ['[email protected]', '[email protected]']

spec.summary = 'FancyLoader efficiently batches queries using postgres window functions to allow advanced features such as orders, limits, pagination, and authorization scoping.'
spec.description = 'FancyLoader (built on top of the graphql-batch gem) efficiently batches queries using postgres window functions to allow advanced features such as orders, limits, pagination, and authorization scoping. Built on top of Arel, FancyLoader is highly extensible and capable of handling complex sorts (including sorting based on a join) with minimal effort and high performance.'
spec.homepage = 'https://github.com/hummingbird-me/graphql-fancy-loader'
spec.license = 'Apache-2.0'
spec.summary = 'FancyLoader efficiently batches queries using postgres window functions to allow advanced features such as orders, limits, pagination, and authorization scoping.'
spec.description = "FancyLoader (built on top of graphql-ruby's dataloader) efficiently batches queries using postgres window functions to allow advanced features such as orders, limits, pagination, and authorization scoping. Built on top of Arel, FancyLoader is highly extensible and capable of handling complex sorts (including sorting based on a join) with minimal effort and high performance."
spec.homepage = 'https://github.com/metafy-gg/graphql-fancy-loader'
spec.license = 'Apache-2.0'

spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['source_code_uri'] = 'https://github.com/hummingbird-me/graphql-fancy-loader'
spec.metadata['source_code_uri'] = 'https://github.com/metafy-gg/graphql-fancy-loader'
spec.metadata['allowed_push_host'] = 'https://rubygems.org'

# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(File.expand_path(__dir__)) do
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end
spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']

spec.add_runtime_dependency 'activesupport', '>= 5.0', '< 9.0'
spec.add_runtime_dependency 'graphql', '>= 1.3', '< 3'
spec.add_runtime_dependency 'graphql-batch', '>= 0.4.3', '< 1'
spec.add_runtime_dependency 'graphql', '>= 1.8', '< 3'

spec.add_development_dependency 'bundler', '~> 2.0'
spec.add_development_dependency 'pry-byebug', '~> 3.10'
spec.add_development_dependency 'bundler', '>= 2.0'
spec.add_development_dependency 'pry-byebug', '~> 3.11'
spec.add_development_dependency 'rake', '>= 13.2'
spec.add_development_dependency 'rspec', '~> 3.13'
spec.add_development_dependency 'simplecov', '~> 0.22'
Expand All @@ -43,8 +42,8 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'bootsnap', '~> 1.18'
spec.add_development_dependency 'database_cleaner', '~> 2.1'
spec.add_development_dependency 'factory_bot_rails', '~> 6.4'
spec.add_development_dependency 'listen', '~> 3.9'
spec.add_development_dependency 'listen', '>= 3.9'
spec.add_development_dependency 'pg', '~> 1.5'
spec.add_development_dependency 'rails', '8.0'
spec.add_development_dependency 'rspec-rails', '~> 7.0'
spec.add_development_dependency 'rails', '>= 8.0'
spec.add_development_dependency 'rspec-rails', '>= 7.0'
end
63 changes: 30 additions & 33 deletions lib/graphql/fancy_connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,64 +9,61 @@ def initialize(loader, args, key, **super_args)
super(nil, **super_args)
end

# @return [Promise<Array<ApplicationRecord>>]
# @return [Array<ApplicationRecord>]
def nodes
resolved_nodes = base_nodes
if @then
base_nodes.then(@then)
@then.call(resolved_nodes)
else
base_nodes
resolved_nodes
end
end

def edges
@edges ||= nodes.then do |nodes|
nodes.map { |n| @edge_class.new(n, self) }
@edges ||= begin
resolved_nodes = nodes
resolved_nodes.map { |n| @edge_class.new(n, self) }
end
end

# @return [Promise<Integer>]
# @return [Integer]
def total_count
base_nodes.then do |results|
if results.first
results.first.attributes['total_count']
else
0
end
results = base_nodes
if results.first
results.first.attributes['total_count']
else
0
end
end

# @return [Promise<Boolean>]
# @return [Boolean]
def has_next_page # rubocop:disable Naming/PredicateName
base_nodes.then do |results|
if results.last
results.last.attributes['row_number'] < results.last.attributes['total_count']
else
false
end
results = base_nodes
if results.last
results.last.attributes['row_number'] < results.last.attributes['total_count']
else
false
end
end

# @return [Promise<Boolean>]
# @return [Boolean]
def has_previous_page # rubocop:disable Naming/PredicateName
base_nodes.then do |results|
if results.first
results.first.attributes['row_number'] > 1
else
false
end
results = base_nodes
if results.first
results.first.attributes['row_number'] > 1
else
false
end
end

def start_cursor
base_nodes.then do |results|
cursor_for(results.first)
end
results = base_nodes
cursor_for(results.first)
end

def end_cursor
base_nodes.then do |results|
cursor_for(results.last)
end
results = base_nodes
cursor_for(results.last)
end

def cursor_for(item)
Expand All @@ -81,7 +78,7 @@ def then(&block)
private

def base_nodes
@base_nodes ||= @loader.for(**loader_args).load(@key)
@base_nodes ||= context.dataloader.with(@loader, **loader_args).load(@key)
end

def after_offset
Expand Down
16 changes: 7 additions & 9 deletions lib/graphql/fancy_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
# To use +FancyLoader+, you'll make a subclass to define your sorts and source model. You can then
# create a field which uses your subclass to load data.

require 'graphql/batch'
require 'graphql'
require 'active_support'
require 'active_support/concern'
require 'active_support/configurable'
require 'active_support/core_ext/class/attribute'

require 'graphql/fancy_loader/version'
require 'graphql/fancy_loader/dsl'

module GraphQL
class FancyLoader < GraphQL::Batch::Loader
class FancyLoader < GraphQL::Dataloader::Source
include ActiveSupport::Configurable
include GraphQL::FancyLoader::DSL
include DSL

config_accessor :middleware

Expand Down Expand Up @@ -61,8 +62,8 @@ def initialize(find_by:, sort:, before: nil, after: 0, first: nil, last: nil, wh
end

# Perform the loading. Uses {GraphQL::FancyLoader::QueryGenerator} to build a query, then groups
# the results by the @find_by column, then fulfills all the Promises.
def perform(keys)
# the results by the @find_by column, then returns the grouped results.
def fetch(keys)
query = QueryGenerator.new(
model: model,
find_by: @find_by,
Expand All @@ -78,9 +79,7 @@ def perform(keys)
).query

results = query.to_a.group_by { |rec| rec[@find_by] }
keys.each do |key|
fulfill(key, results[key] || [])
end
keys.map { |key| results[key] || [] }
end

private
Expand All @@ -100,6 +99,5 @@ def sort
require 'graphql/fancy_loader/query_generator'
require 'graphql/fancy_loader/rank_query_generator'
require 'graphql/fancy_loader/type_generator'
require 'graphql/fancy_loader/version'
# Middleware
require 'graphql/fancy_loader/pundit_middleware'
2 changes: 1 addition & 1 deletion lib/graphql/fancy_loader/dsl.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module GraphQL
class FancyLoader < GraphQL::Batch::Loader
class FancyLoader
module DSL
extend ActiveSupport::Concern

Expand Down
7 changes: 3 additions & 4 deletions lib/graphql/fancy_loader/version.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
module GraphQL
# HACK: This allows us to import the version number in the gemspec
class FancyLoader < (begin
require 'graphql/batch'
GraphQL::Batch::Loader
require 'graphql/dataloader'
GraphQL::Dataloader::Source
rescue LoadError
BasicObject
end)
VERSION = '0.1.6'.freeze # .3
VERSION = '0.2.0'.freeze
end
end
2 changes: 2 additions & 0 deletions spec/dummy/config/database.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
default: &default
adapter: postgresql
encoding: unicode
host: localhost
port: 5432
prepared_statements: false
advisory_locks: false
<% if ENV.include?('DATABASE_URL') %>
Expand Down
13 changes: 6 additions & 7 deletions spec/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,23 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2021_10_17_215654) do

ActiveRecord::Schema[8.0].define(version: 2021_10_17_215654) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_catalog.plpgsql"

create_table "posts", force: :cascade do |t|
t.bigint "user_id"
t.string "title", null: false
t.string "description"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_posts_on_user_id"
end

create_table "users", force: :cascade do |t|
t.string "email", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

add_foreign_key "posts", "users"
Expand Down