diff --git a/.gitignore b/.gitignore index 0cb6eeb..11a84d5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /pkg/ /spec/reports/ /tmp/ +.rspec_status +/gemfiles/*.lock diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index cd57a8b..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.1.5 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..28c90c0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,41 @@ +--- +language: ruby +cache: bundler +before_install: gem install bundler -v '~> 2.0' + +jobs: + include: + - rvm: 2.7.0 + gemfile: gemfiles/graphql_1.10.gemfile + env: + - GRAPHQL_RUBY_INTERPRETER=yes + - RAILS_VERSION=6.0 + - rvm: 2.7.0 + gemfile: gemfiles/graphql_1.10.gemfile + env: + - GRAPHQL_RUBY_INTERPRETER=no + - RAILS_VERSION=6.0 + - rvm: 2.6.5 + gemfile: gemfiles/graphql_1.9.gemfile + env: + - GRAPHQL_RUBY_INTERPRETER=yes + - RAILS_VERSION=6.0 + - rvm: 2.6.5 + gemfile: gemfiles/graphql_1.9.gemfile + env: + - GRAPHQL_RUBY_INTERPRETER=no + - RAILS_VERSION=6.0 + - rvm: 2.5.7 + gemfile: gemfiles/graphql_1.9.gemfile + env: + - GRAPHQL_RUBY_INTERPRETER=yes + - RAILS_VERSION=5.2 + - rvm: 2.5.7 + gemfile: gemfiles/graphql_1.9.gemfile + env: + - GRAPHQL_RUBY_INTERPRETER=no + - RAILS_VERSION=5.2 + - rvm: 2.4.9 + gemfile: gemfiles/graphql_1.8.gemfile + env: + - RAILS_VERSION=5.1 diff --git a/Appraisals b/Appraisals new file mode 100644 index 0000000..3cef181 --- /dev/null +++ b/Appraisals @@ -0,0 +1,11 @@ +appraise "graphql-1.8" do + gem "graphql", "~> 1.8.0" +end + +appraise "graphql-1.9" do + gem "graphql", "~> 1.9.0" +end + +appraise "graphql-1.10" do + gem "graphql", "~> 1.10.0" +end diff --git a/Gemfile b/Gemfile index 87e9ddf..1558cbb 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,5 @@ source 'https://rubygems.org' # Specify your gem's dependencies in graphql-preload.gemspec gemspec + +gem 'activerecord', ENV['RAILS_VERSION'] && "~> #{ENV['RAILS_VERSION']}.0" diff --git a/Rakefile b/Rakefile index 4b2b782..c24ed53 100644 --- a/Rakefile +++ b/Rakefile @@ -1,10 +1,6 @@ require 'bundler/gem_tasks' -require 'rake/testtask' +require 'rspec/core/rake_task' -Rake::TestTask.new(:test) do |t| - t.libs << 'test' - t.libs << 'lib' - t.test_files = FileList['test/**/*_test.rb'] -end +RSpec::Core::RakeTask.new -task default: :test +task default: :spec diff --git a/gemfiles/.bundle/config b/gemfiles/.bundle/config new file mode 100644 index 0000000..c127f80 --- /dev/null +++ b/gemfiles/.bundle/config @@ -0,0 +1,2 @@ +--- +BUNDLE_RETRY: "1" diff --git a/gemfiles/graphql_1.10.gemfile b/gemfiles/graphql_1.10.gemfile new file mode 100644 index 0000000..9e3e21a --- /dev/null +++ b/gemfiles/graphql_1.10.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activerecord", ENV['RAILS_VERSION'] && "~> #{ENV['RAILS_VERSION']}.0" +gem "graphql", "~> 1.10.0" + +gemspec path: "../" diff --git a/gemfiles/graphql_1.8.gemfile b/gemfiles/graphql_1.8.gemfile new file mode 100644 index 0000000..9b94291 --- /dev/null +++ b/gemfiles/graphql_1.8.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activerecord", ENV['RAILS_VERSION'] && "~> #{ENV['RAILS_VERSION']}.0" +gem "graphql", "~> 1.8.0" + +gemspec path: "../" diff --git a/gemfiles/graphql_1.9.gemfile b/gemfiles/graphql_1.9.gemfile new file mode 100644 index 0000000..7eb4a56 --- /dev/null +++ b/gemfiles/graphql_1.9.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "activerecord", ENV['RAILS_VERSION'] && "~> #{ENV['RAILS_VERSION']}.0" +gem "graphql", "~> 1.9.0" + +gemspec path: "../" diff --git a/graphql-preload.gemspec b/graphql-preload.gemspec index c2bd62d..d5aae55 100644 --- a/graphql-preload.gemspec +++ b/graphql-preload.gemspec @@ -26,9 +26,12 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'graphql-batch', '~> 0.3' spec.add_runtime_dependency 'promise.rb', '~> 0.7' - spec.add_development_dependency 'bundler', '~> 1.16' - spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'bundler', '~> 2.0' spec.add_development_dependency 'pry', '~> 0.10' - spec.add_development_dependency 'rake', '~> 10.0' + spec.add_development_dependency 'rake', '~> 13.0' + spec.add_development_dependency 'rspec', '~> 3.8' + spec.add_development_dependency 'rspec-sqlimit' + spec.add_development_dependency 'sqlite3' spec.add_development_dependency 'yard', '~> 0.9' + spec.add_development_dependency "appraisal" end diff --git a/spec/graphql/preload_spec.rb b/spec/graphql/preload_spec.rb new file mode 100644 index 0000000..d758e5c --- /dev/null +++ b/spec/graphql/preload_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require "spec_helper" + +RSpec.describe GraphQL::Preload do + subject do + @result ||= + schema.execute(query: query_string, context: {}, variables: {}) + end + + shared_examples "test suite" do + context "without associations" do + let(:query_string) { "query { products { title } }" } + + it "doesn't load associations at all" do + expect { subject }.not_to exceed_query_limit 1 + end + end + + context "with associations" do + let(:query_string) do + <<~QRAPHQL + query { posts { title comments { text } } } + QRAPHQL + end + + it "preloads associations by single query" do + expect { subject }.to exceed_query_limit 1 # to ensure that GraphQL query at least works + expect { subject }.not_to exceed_query_limit 2 + posts = subject.dig("data", "posts") + expect(posts.size).to eq(4) + expect(posts.flat_map { |p| p["comments"] }.size).to eq(8) + end + end + + context "with associations with custom scopes" do + let(:query_string) do + <<~QRAPHQL + query { users { name posts { title } } } + QRAPHQL + end + + it "preloads associations by single query and given order" do + expect { subject }.not_to exceed_query_limit 2 + posts = subject.dig("data", "users").flat_map { |p| p["posts"] } + # Posts for every user should be from greater rating to lower + expect(posts.map { |p| p["title"] }).to eq(%w[Bar Foo Baz Huh]) + end + end + end + + context "modern class-based GraphQL schema" do + let(:schema) { PreloadSchema } + + include_examples "test suite" + end + + context "legacy define-based GraphQL schema" do + next if TESTING_GRAPHQL_RUBY_INTERPRETER # No interpreter runtime these days + + let(:schema) { Legacy::PreloadSchema } + + include_examples "test suite" + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..6389fd8 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "bundler/setup" +require "graphql/preload" +require "rspec-sqlimit" +require "pry" +require "yaml" + +TESTING_GRAPHQL_RUBY_INTERPRETER = + begin + env_value = ENV["GRAPHQL_RUBY_INTERPRETER"] + env_value ? YAML.safe_load(env_value) : false + end + +require_relative "support/database" +require_relative "support/graphql_schema" +require_relative "support/legacy_graphql_schema" + +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec do |c| + c.syntax = :expect + end + + config.mock_with :rspec + + Kernel.srand config.seed + config.order = :random +end diff --git a/spec/support/database.rb b/spec/support/database.rb new file mode 100644 index 0000000..88d783a --- /dev/null +++ b/spec/support/database.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +require "active_record" +require "sqlite3" + +ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + +ActiveRecord::Schema.define(version: 0) do + create_table :users do |t| + t.string :name + end + + create_table :posts do |t| + t.string :title + t.text :text + t.float :rating + t.references :author, foreign_key: { to_table: :users, column: :author_id, on_delete: :cascade } + end + + create_table :comments do |t| + t.text :text + t.references :post, foreign_key: { on_delete: :cascade } + t.references :author, foreign_key: { to_table: :users, column: :author_id, on_delete: :cascade } + end +end + +class User < ActiveRecord::Base + has_many :posts, inverse_of: :author, foreign_key: :author_id + has_many :comments, inverse_of: :author, foreign_key: :author_id +end + +class Post < ActiveRecord::Base + belongs_to :author, class_name: "User" + has_many :comments +end + +class Comment < ActiveRecord::Base + belongs_to :author, class_name: "User" + belongs_to :post +end + +alice, bob = User.create!([{ name: 'Alice' }, { name: 'Bob' }]) + +alice.posts.create!([{ title: "Foo", rating: 4 }, { title: "Bar", rating: 8 }]) +bob.posts.create!([{ title: "Baz", rating: 7 }, { title: "Huh", rating: 4.2 }]) + +Post.all.each do |post| + alice.comments.create(post: post, text: "Great post!") + bob.comments.create(post: post, text: "Great post!") +end diff --git a/spec/support/graphql_schema.rb b/spec/support/graphql_schema.rb new file mode 100644 index 0000000..fcd303b --- /dev/null +++ b/spec/support/graphql_schema.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class CommentType < GraphQL::Schema::Object + field :id, ID, null: false + field :text, String, null: false + field :author_id, ID, null: false + field :post_id, ID, null: false +end + +class PostType < GraphQL::Schema::Object + field :id, ID, null: false + field :title, String, null: false + field :text, String, null: false + field :rating, Float, null: false + field :comments, [CommentType], null: false, preload: :comments + field :author_id, ID, null: false +end + +class UserType < GraphQL::Schema::Object + field :id, ID, null: false + field :name, String, null: false + field :comments, [CommentType], null: false, preload: :comments + field :posts, [PostType], null: false, + preload: :posts, + preload_scope: ->(*) { Post.order(rating: :desc) } +end + +class QueryType < GraphQL::Schema::Object + field :users, [UserType], null: false + field :posts, [PostType], null: false + + def users + User.all + end + + def posts + Post.all + end +end + +class PreloadSchema < GraphQL::Schema + use GraphQL::Batch + enable_preloading + + if TESTING_GRAPHQL_RUBY_INTERPRETER + use GraphQL::Execution::Interpreter + use GraphQL::Analysis::AST + end + + query QueryType +end diff --git a/spec/support/legacy_graphql_schema.rb b/spec/support/legacy_graphql_schema.rb new file mode 100644 index 0000000..c03dfe9 --- /dev/null +++ b/spec/support/legacy_graphql_schema.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Legacy + CommentType = GraphQL::ObjectType.define do + name 'Comment' + + field :id, !types.ID + field :text, !types.String + field :author_id, !types.ID + field :post_id, !types.ID + end + + PostType = GraphQL::ObjectType.define do + name 'Post' + + field :id, !types.ID + field :title, !types.String + field :text, !types.String + field :rating, !types.String + field :author_id, !types.ID + field :comments, !types[!CommentType] do + # Post.includes(:comments) + preload :comments + + resolve ->(obj, _args, _ctx) { obj.comments } + end + end + + + UserType = GraphQL::ObjectType.define do + name 'User' + + field :id, !types.ID + field :name, !types.String + field :comments, !types[!CommentType] do + preload :comments + + resolve ->(obj, _args, _ctx) { obj.comments } + end + + field :posts, !types[!PostType] do + preload :posts + preload_scope ->(*) { Post.order(rating: :desc) } + + resolve ->(obj, _args, _ctx) { obj.posts } + end + end + + QueryType = GraphQL::ObjectType.define do + name 'Query' + + field :users do + type !types[!UserType] + resolve ->(*) { User.all } + end + + field :posts do + type !types[!PostType] + resolve ->(*) { Post.all } + end + end + + PreloadSchema = GraphQL::Schema.define do + use GraphQL::Batch + + enable_preloading + + query QueryType + end +end \ No newline at end of file diff --git a/test/graphql/preload_test.rb b/test/graphql/preload_test.rb deleted file mode 100644 index 35280d3..0000000 --- a/test/graphql/preload_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'test_helper' - -module GraphQL - class PreloadTest < Minitest::Test - def test_that_it_has_a_version_number - refute_nil ::GraphQL::Preload::VERSION - end - end -end diff --git a/test/test_helper.rb b/test/test_helper.rb deleted file mode 100644 index e25a6a0..0000000 --- a/test/test_helper.rb +++ /dev/null @@ -1,4 +0,0 @@ -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) -require 'graphql/preload' - -require 'minitest/autorun'