Skip to content

Commit 1003df4

Browse files
committed
Add tests for field dataload: shortcuts
1 parent a511437 commit 1003df4

6 files changed

Lines changed: 119 additions & 46 deletions

File tree

lib/graphql/schema/field.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def method_conflict_warning?
197197
# @param resolve_batch [Symbol, true, nil] Used by {Schema.execute_next} map `objects` to a same-sized Array of results. Called on the owner type class with `objects, context, **arguments`.
198198
# @param resolve_each [Symbol, true, nil] Used by {Schema.execute_next} to get a value value for each item. Called on the owner type class with `object, context, **arguments`.
199199
# @param resolve_legacy_instance_method [Symbol, true, nil] Used by {Schema.execute_next} to get a value value for each item. Calls an instance method on the object type class.
200+
# @param dataload [Class, Hash] Shorthand for making dataloader calls
200201
# @param max_page_size [Integer, nil] For connections, the maximum number of items to return from this field, or `nil` to allow unlimited results.
201202
# @param default_page_size [Integer, nil] For connections, the default number of items to return from this field, or `nil` to return unlimited results.
202203
# @param introspection [Boolean] If true, this field will be marked as `#introspection?` and the name may begin with `__`
@@ -219,7 +220,7 @@ def method_conflict_warning?
219220
# @param relay_nodes_field [Boolean] (Private, used by GraphQL-Ruby)
220221
# @param extras [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] Extra arguments to be injected into the resolver for this field
221222
# @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.
222-
def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, resolve_legacy_instance_method: nil, resolve_static: nil, resolve_each: nil, resolve_batch: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
223+
def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, resolve_legacy_instance_method: nil, resolve_static: nil, resolve_each: nil, resolve_batch: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, dataload: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
223224
if name.nil?
224225
raise ArgumentError, "missing first `name` argument or keyword `name:`"
225226
end
@@ -289,6 +290,9 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CON
289290
elsif resolve_legacy_instance_method
290291
@execution_next_mode = :resolve_legacy_instance_method
291292
@execution_next_mode_key = resolve_legacy_instance_method == true ? @method_sym : resolve_legacy_instance_method
293+
elsif dataload
294+
@execution_next_mode = :dataload
295+
@execution_next_mode_key = dataload
292296
else
293297
@execution_next_mode = :direct_send
294298
@execution_next_mode_key = @method_sym

lib/graphql/schema/member/has_fields.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ module HasFields
4848
# @option kwargs [Boolean] :dynamic_introspection (Private, used by GraphQL-Ruby)
4949
# @option kwargs [Boolean] :relay_node_field (Private, used by GraphQL-Ruby)
5050
# @option kwargs [Boolean] :relay_nodes_field (Private, used by GraphQL-Ruby)
51+
# @option kwargs [Class, Hash] :dataload Shorthand for dataloader lookups
5152
# @option kwargs [Array<:ast_node, :parent, :lookahead, :owner, :execution_errors, :graphql_name, :argument_details, Symbol>] :extras Extra arguments to be injected into the resolver for this field
5253
# @param kwargs [Hash] Keywords for defining the field. Any not documented here will be passed to your base field class where they must be handled.
5354
# @param definition_block [Proc] an additional block for configuring the field. Receive the field as a block param, or, if no block params are defined, then the block is `instance_eval`'d on the new {Field}.

spec/graphql/dataloader/active_record_association_source_spec.rb

Lines changed: 19 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,7 @@
33

44
describe GraphQL::Dataloader::ActiveRecordAssociationSource do
55
if testing_rails?
6-
class VulfpeckSchema < GraphQL::Schema
7-
class Album < GraphQL::Schema::Object
8-
field :name, String
9-
end
10-
class Band < GraphQL::Schema::Object
11-
field :albums, [Album] do
12-
argument :genre, String, required: false
13-
argument :reverse, Boolean, required: false, default_value: false
14-
argument :unscoped, Boolean, required: false, default_value: false
15-
end
16-
17-
def albums(genre: nil, reverse:, unscoped:)
18-
if unscoped
19-
scope = nil
20-
else
21-
scope = ::Album
22-
if genre
23-
scope = scope.where(band_genre: genre)
24-
end
25-
26-
scope = if reverse
27-
scope.order(name: :desc)
28-
else
29-
scope.order(:name)
30-
end
31-
end
32-
dataload_association(:albums, scope: scope)
33-
end
34-
end
35-
36-
class Query < GraphQL::Schema::Object
37-
field :band, Band do
38-
argument :name, String
39-
end
40-
41-
def band(name:)
42-
::Band.find_by(name: name)
43-
end
44-
end
45-
46-
query(Query)
47-
use GraphQL::Dataloader
48-
end
49-
6+
include VulfpeckSchemaHelpers
507
it "works with different scopes on the same object at runtime" do
518
query_str = <<~GRAPHQL
529
{
@@ -67,13 +24,30 @@ def band(name:)
6724
}
6825
GRAPHQL
6926

70-
result = VulfpeckSchema.execute(query_str)
27+
result = exec_query(query_str)
7128
assert_equal ["Mit Peck", "My First Car"], result["data"]["band"]["allAlbums"].map { |a| a["name"] }
7229
assert_equal ["Mit Peck", "My First Car"], result["data"]["band"]["unscopedAlbums"].map { |a| a["name"] }
7330
assert_equal ["My First Car", "Mit Peck"], result["data"]["band"]["reverseAlbums"].map { |a| a["name"] }
7431
assert_equal [], result["data"]["band"]["countryAlbums"]
7532
end
7633

34+
it "works with field shorthands" do
35+
skip("NOT IMPLEMENTED") unless TESTING_EXEC_NEXT
36+
result = exec_query <<-GRAPHQL
37+
{
38+
band(name: "Vulfpeck") {
39+
allAlbums {
40+
name
41+
band { name }
42+
}
43+
}
44+
}
45+
GRAPHQL
46+
47+
assert_equal ["Mit Peck", "My First Car"], result["data"]["band"]["allAlbums"].map { |a| a["name"] }
48+
assert_equal ["Vulfpeck", "Vulfpeck"], result["data"]["band"]["allAlbums"].map { |a| a["band"]["name"] }
49+
end
50+
7751
it_dataloads "queries for associated records when the association isn't already loaded" do |d|
7852
my_first_car = ::Album.find(2)
7953
homey = ::Album.find(4)

spec/graphql/dataloader/active_record_source_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33

44
describe GraphQL::Dataloader::ActiveRecordSource do
55
if testing_rails?
6+
include VulfpeckSchemaHelpers
7+
it "works with field config shorthands" do
8+
skip("Not implemented") unless TESTING_EXEC_NEXT
9+
query_str = "{ rootBand { name } }"
10+
assert_equal "Wilco", exec_query(query_str, root_value: OpenStruct.new(band_name: "Wilco"))["data"]["rootBand"]["name"]
11+
assert_equal "Chon", exec_query(query_str, root_value: OpenStruct.new(band_name: "Chon"))["data"]["rootBand"]["name"]
12+
end
13+
614
describe "finding by ID" do
715
it_dataloads "loads once, then returns from a cache when available" do |d|
816
log = with_active_record_log(colorize: false) do

spec/graphql/dataloader/source_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ def fetch(keys)
88
end
99
end
1010

11+
if testing_rails?
12+
describe "with field configuration shorthands" do
13+
include VulfpeckSchemaHelpers
14+
it "calls the configured source" do
15+
skip("Not implemented") unless TESTING_EXEC_NEXT
16+
result = exec_query("{ bandsCount albumsCount }")
17+
assert_equal 4, result["data"]["bandsCount"]
18+
assert_equal 6, result["data"]["albumsCount"]
19+
end
20+
end
21+
end
22+
1123
it "raises an error when it tries too many times to sync" do
1224
dl = GraphQL::Dataloader.new
1325
dl.append_job { dl.with(FailsToLoadSource).load(1) }

spec/support/active_record_setup.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,80 @@ class CompositeBand < Band
163163
c.albums.create!(id: 4, name: "Homey")
164164
c.albums.create!(id: 5, name: "Chon")
165165
w.albums.create!(id: 6, name: "Summerteeth")
166+
167+
module VulfpeckSchemaHelpers
168+
class VulfpeckSchema < GraphQL::Schema
169+
class ModelCountSource < GraphQL::Dataloader::Source
170+
def initialize(model)
171+
@model = model
172+
end
173+
174+
def fetch(objects)
175+
result = @model.count
176+
Array.new(objects.size, result)
177+
end
178+
end
179+
180+
class Album < GraphQL::Schema::Object
181+
field :name, String
182+
field :band, "VulfpeckSchemaHelpers::VulfpeckSchema::Band", dataload: { association: true }
183+
end
184+
class Band < GraphQL::Schema::Object
185+
field :name, String
186+
field :albums, [Album], resolve_legacy_instance_method: true do
187+
argument :genre, String, required: false
188+
argument :reverse, Boolean, required: false, default_value: false
189+
argument :unscoped, Boolean, required: false, default_value: false
190+
end
191+
192+
def albums(genre: nil, reverse:, unscoped:)
193+
if unscoped
194+
scope = nil
195+
else
196+
scope = ::Album
197+
if genre
198+
scope = scope.where(band_genre: genre)
199+
end
200+
201+
scope = if reverse
202+
scope.order(name: :desc)
203+
else
204+
scope.order(:name)
205+
end
206+
end
207+
dataload_association(:albums, scope: scope)
208+
end
209+
210+
field :all_albums, [Album], dataload: { association: :albums }
211+
end
212+
213+
class Query < GraphQL::Schema::Object
214+
field :band, Band, resolve_legacy_instance_method: true do
215+
argument :name, String
216+
end
217+
218+
def band(name:)
219+
::Band.find_by(name: name)
220+
end
221+
222+
field :root_band, Band, dataload: { model: ::Band, using: :band_name, find_by: :name }
223+
field :bands_count, Integer, dataload: { with: ModelCountSource, by: [::Band]}
224+
field :albums_count, Integer, dataload: { with: ModelCountSource, by: [::Album]}
225+
end
226+
227+
query(Query)
228+
use GraphQL::Dataloader
229+
use GraphQL::Execution::Next
230+
end
231+
232+
def exec_query(...)
233+
if TESTING_EXEC_NEXT
234+
VulfpeckSchema.execute_next(...)
235+
else
236+
VulfpeckSchema.execute(...)
237+
end
238+
end
239+
end
166240
class Author < ActiveRecord::Base
167241
has_many :books
168242

0 commit comments

Comments
 (0)