Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support interfaces implementing other interfaces #3613

Merged
merged 10 commits into from Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yaml
@@ -1,6 +1,5 @@
name: CI Suite
on:
- push
- pull_request

jobs:
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Expand Up @@ -62,7 +62,7 @@ ERR
end

assert_dependency_version("Ragel", "7.0.0.9", "ragel -v")
assert_dependency_version("Racc", "1.4.16", %|ruby -e "require 'racc'; puts Racc::VERSION"|)
assert_dependency_version("Racc", "1.5.2", %|ruby -e "require 'racc'; puts Racc::VERSION"|)

`rm -f lib/graphql/language/parser.rb lib/graphql/language/lexer.rb `
`racc lib/graphql/language/parser.y -o lib/graphql/language/parser.rb`
Expand Down
73 changes: 73 additions & 0 deletions guides/dataloader/testing.md
@@ -0,0 +1,73 @@
---
layout: guide
search: true
section: Dataloader
title: Testing
desc: Tips for testing Dataloader implementation
index: 4
experimental: true
---

There are a few techniques for testing your {{ "GraphQL::Dataloader" | api_doc }} setup.

## Integration Tests

One important feature of `Dataloader` is how it manages database access while GraphQL runs queries. You can test that by listening for database queries while running queries, for example, with ActiveRecord:


```ruby
def test_active_record_queries_are_batched_and_cached
# set up a listener function
database_queries = 0
callback = lambda {|_name, _started, _finished, _unique_id, _payload| database_queries += 1 }

query_str = <<-GRAPHQL
{
a1: author(id: 1) { name }
a2: author(id: 2) { name }
b1: book(id: 1) { author { name } }
b2: book(id: 2) { author { name } }
}
GRAPHQL

# Run the query with the listener
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
MySchema.execute(query_str)
end

# One query for authors, one query for books
assert_equal 2, database_queries
end
```

You could also make specific assertions on the queries that are run (see the [`sql.active_record` docs](https://edgeguides.rubyonrails.org/active_support_instrumentation.html#active-record)). For other frameworks and databases, check your ORM or library for instrumentation options.

## Testing Dataloader Sources

You can also test `Dataloader` behavior outside of GraphQL using {{ "GraphQL::Dataloader.with_dataloading" | api_doc }}. For example:

```ruby
def test_it_fetches_objects_by_id
user_1, user_2, user_3 = 3.times { User.create! }

database_queries = 0
callback = lambda {|_name, _started, _finished, _unique_id, _payload| database_queries += 1 }

ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
GraphQL::Dataloader.with_dataloading do |dataloader|
req1 = dataloader.with(Sources::ActiveRecord).request(user_1.id)
req2 = dataloader.with(Sources::ActiveRecord).request(user_3.id)
req3 = dataloader.with(Sources::ActiveRecord).request(user_2.id)
req4 = dataloader.with(Sources::ActiveRecord).request(-1)

# Validate source's matching up of records
expect(req1.load).to eq(user_1)
expect(req2.load).to eq(user_3)
expect(req3.load).to eq(user_2)
expect(req4.load).to be_nil
end
end

assert_equal 1, database_queries, "All users were looked up at once"
end
```
12 changes: 12 additions & 0 deletions lib/graphql/dataloader.rb
Expand Up @@ -27,6 +27,18 @@ def self.use(schema)
schema.dataloader_class = self
end

# Call the block with a Dataloader instance,
# then run all enqueued jobs and return the result of the block.
def self.with_dataloading(&block)
dataloader = self.new
result = nil
dataloader.append_job {
result = block.call(dataloader)
}
dataloader.run
result
end

def initialize
@source_cache = Hash.new { |h, source_class| h[source_class] = Hash.new { |h2, batch_parameters|
source = if RUBY_VERSION < "3"
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/define/instance_definable.rb
Expand Up @@ -76,7 +76,7 @@ def ensure_defined
# Apply definition from `define(...)` kwargs
defn.define_keywords.each do |keyword, value|
# Don't splat string hashes, which blows up on Rubies before 2.7
if value.is_a?(Hash) && value.each_key.all? { |k| k.is_a?(Symbol) }
if value.is_a?(Hash) && !value.empty? && value.each_key.all? { |k| k.is_a?(Symbol) }
defn_proxy.public_send(keyword, **value)
else
defn_proxy.public_send(keyword, value)
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/introspection/type_type.rb
Expand Up @@ -50,7 +50,7 @@ def enum_values(include_deprecated:)
end

def interfaces
if @object.kind == GraphQL::TypeKinds::OBJECT
if @object.kind.object? || @object.kind.interface?
@context.warden.interfaces(@object)
else
nil
Expand Down
1 change: 1 addition & 0 deletions lib/graphql/language/document_from_schema_definition.rb
Expand Up @@ -87,6 +87,7 @@ def build_union_type_node(union_type)
def build_interface_type_node(interface_type)
GraphQL::Language::Nodes::InterfaceTypeDefinition.new(
name: interface_type.graphql_name,
interfaces: warden.interfaces(interface_type).sort_by(&:graphql_name).map { |type| build_type_name_node(type) },
description: interface_type.description,
fields: build_field_nodes(warden.fields(interface_type)),
directives: directives(interface_type),
Expand Down
2 changes: 2 additions & 0 deletions lib/graphql/language/nodes.rb
Expand Up @@ -618,6 +618,7 @@ class InterfaceTypeDefinition < AbstractNode
attr_reader :description
scalar_methods :name
children_methods({
interfaces: GraphQL::Language::Nodes::TypeName,
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
Expand All @@ -627,6 +628,7 @@ class InterfaceTypeDefinition < AbstractNode
class InterfaceTypeExtension < AbstractNode
scalar_methods :name
children_methods({
interfaces: GraphQL::Language::Nodes::TypeName,
directives: GraphQL::Language::Nodes::Directive,
fields: GraphQL::Language::Nodes::FieldDefinition,
})
Expand Down