Skip to content

Commit a7baaec

Browse files
committed
Require loaders to be used within a GraphQL::Batch.batch block
This is needed to avoid global state leaking between requests/tests.
1 parent 902f3ee commit a7baaec

File tree

8 files changed

+398
-347
lines changed

8 files changed

+398
-347
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,13 +109,19 @@ end
109109

110110
## Unit Testing
111111

112-
Promise#sync can be used to wait for a promise to be resolved and return its result. This can be useful for debugging and unit testing loaders.
112+
Your loaders can be tested outside of a GraphQL query by doing the
113+
batch loads in a block passed to GraphQL::Batch.batch. That method
114+
will set up thread-local state to store the loaders, batch load any
115+
promise returned from the block then clear the thread-local state
116+
to avoid leaking state between tests.
113117

114118
```ruby
115119
def test_single_query
116120
product = products(:snowboard)
117-
query = RecordLoader.for(Product).load(args["id"]).then(&:title)
118-
assert_equal product.title, query.sync
121+
title = GraphQL::Batch.batch do
122+
RecordLoader.for(Product).load(product.id).then(&:title)
123+
end
124+
assert_equal product.title, title
119125
end
120126
```
121127

lib/graphql/batch.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44
module GraphQL
55
module Batch
66
BrokenPromiseError = ::Promise::BrokenError
7+
class NestedError < StandardError; end
8+
9+
def self.batch
10+
raise NestedError if GraphQL::Batch::Executor.current
11+
begin
12+
GraphQL::Batch::Executor.current = GraphQL::Batch::Executor.new
13+
Promise.sync(yield)
14+
ensure
15+
GraphQL::Batch::Executor.current = nil
16+
end
17+
end
718
end
819
end
920

lib/graphql/batch/execution_strategy.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
module GraphQL::Batch
22
class ExecutionStrategy < GraphQL::Query::SerialExecution
33
def execute(_, _, query)
4-
deep_sync(super)
4+
GraphQL::Batch.batch do
5+
as_promise_unless_resolved(super)
6+
end
57
rescue GraphQL::InvalidNullError => err
68
err.parent_error? || query.context.errors.push(err)
79
nil
8-
ensure
9-
GraphQL::Batch::Executor.current.clear
1010
end
1111

12-
def deep_sync(result)
12+
# Needed for MutationExecutionStrategy
13+
def deep_sync(result) #:nodoc:
1314
Promise.sync(as_promise_unless_resolved(result))
1415
end
1516

lib/graphql/batch/executor.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ class Executor
44
private_constant :THREAD_KEY
55

66
def self.current
7-
Thread.current[THREAD_KEY] ||= new
7+
Thread.current[THREAD_KEY]
8+
end
9+
10+
def self.current=(executor)
11+
Thread.current[THREAD_KEY] = executor
812
end
913

1014
attr_reader :loaders

0 commit comments

Comments
 (0)