Skip to content

Commit

Permalink
Require loaders to be used within a GraphQL::Batch.batch block (#36)
Browse files Browse the repository at this point in the history
This is needed to avoid global state leaking between requests/tests.
  • Loading branch information
dylanahsmith authored Nov 22, 2016
1 parent 902f3ee commit d1454e9
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 347 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,19 @@ end

## Unit Testing

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.
Your loaders can be tested outside of a GraphQL query by doing the
batch loads in a block passed to GraphQL::Batch.batch. That method
will set up thread-local state to store the loaders, batch load any
promise returned from the block then clear the thread-local state
to avoid leaking state between tests.

```ruby
def test_single_query
product = products(:snowboard)
query = RecordLoader.for(Product).load(args["id"]).then(&:title)
assert_equal product.title, query.sync
title = GraphQL::Batch.batch do
RecordLoader.for(Product).load(product.id).then(&:title)
end
assert_equal product.title, title
end
```

Expand Down
11 changes: 11 additions & 0 deletions lib/graphql/batch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
module GraphQL
module Batch
BrokenPromiseError = ::Promise::BrokenError
class NestedError < StandardError; end

def self.batch
raise NestedError if GraphQL::Batch::Executor.current
begin
GraphQL::Batch::Executor.current = GraphQL::Batch::Executor.new
Promise.sync(yield)
ensure
GraphQL::Batch::Executor.current = nil
end
end
end
end

Expand Down
9 changes: 5 additions & 4 deletions lib/graphql/batch/execution_strategy.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
module GraphQL::Batch
class ExecutionStrategy < GraphQL::Query::SerialExecution
def execute(_, _, query)
deep_sync(super)
GraphQL::Batch.batch do
as_promise_unless_resolved(super)
end
rescue GraphQL::InvalidNullError => err
err.parent_error? || query.context.errors.push(err)
nil
ensure
GraphQL::Batch::Executor.current.clear
end

def deep_sync(result)
# Needed for MutationExecutionStrategy
def deep_sync(result) #:nodoc:
Promise.sync(as_promise_unless_resolved(result))
end

Expand Down
6 changes: 5 additions & 1 deletion lib/graphql/batch/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ class Executor
private_constant :THREAD_KEY

def self.current
Thread.current[THREAD_KEY] ||= new
Thread.current[THREAD_KEY]
end

def self.current=(executor)
Thread.current[THREAD_KEY] = executor
end

attr_reader :loaders
Expand Down
Loading

0 comments on commit d1454e9

Please sign in to comment.