graphql-batch 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +23 -0
- data/.rubocop.yml +11 -0
- data/.rubocop_todo.yml +199 -0
- data/Gemfile +4 -2
- data/README.md +13 -30
- data/Rakefile +10 -1
- data/examples/active_storage_loader.rb +75 -0
- data/examples/association_loader.rb +2 -1
- data/examples/http_loader.rb +66 -0
- data/examples/record_loader.rb +1 -0
- data/examples/window_key_loader.rb +81 -0
- data/graphql-batch.gemspec +5 -6
- data/lib/graphql/batch/loader.rb +45 -15
- data/lib/graphql/batch/setup_multiplex.rb +2 -6
- data/lib/graphql/batch/version.rb +1 -1
- data/lib/graphql/batch.rb +8 -18
- metadata +17 -13
- data/.travis.yml +0 -7
- data/lib/graphql/batch/setup.rb +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b257f38787895a689caec957a242bef5fa56e02030b2ac499078a510820b8669
|
4
|
+
data.tar.gz: 9232d5995209ff370617a4f68f31cf936fd334ccd9eaad1cc449336d353a6535
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8106c65d1842f98cbe1698e881e54c569d8452b684a6caea28b62a75cfde21a4b28fdbb6a61acb7a884dc6826a6209d05ddcdc6b3b7843ee6188202dfdfb638
|
7
|
+
data.tar.gz: 370418c233cc8fbf99734ba76eefd1c3d703a0bc40b249ed0dc8e24890a968fe45cf22ff8dc9d1a88c73dee5aa00971c4e3b0a5104048f9b5e3390bfced7a421
|
@@ -0,0 +1,23 @@
|
|
1
|
+
name: Tests
|
2
|
+
|
3
|
+
on:
|
4
|
+
- push
|
5
|
+
- pull_request
|
6
|
+
|
7
|
+
jobs:
|
8
|
+
test:
|
9
|
+
runs-on: ubuntu-latest
|
10
|
+
strategy:
|
11
|
+
fail-fast: false
|
12
|
+
matrix:
|
13
|
+
ruby: [2.4, 2.7, '3.0']
|
14
|
+
graphql_version: ['~> 1.10.0', '~> 1.13']
|
15
|
+
steps:
|
16
|
+
- uses: actions/checkout@v2
|
17
|
+
- uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
bundler-cache: true
|
20
|
+
ruby-version: ${{ matrix.ruby }}
|
21
|
+
env:
|
22
|
+
GRAPHQL_VERSION: ${{ matrix.graphql_version }}
|
23
|
+
- run: bundle exec rake
|
data/.rubocop.yml
ADDED
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2020-02-06 13:18:09 -0500 using RuboCop version 0.78.0.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 1
|
10
|
+
# Cop supports --auto-correct.
|
11
|
+
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
12
|
+
# SupportedStyles: outdent, indent
|
13
|
+
Layout/AccessModifierIndentation:
|
14
|
+
Exclude:
|
15
|
+
- 'examples/http_loader.rb'
|
16
|
+
|
17
|
+
# Offense count: 5
|
18
|
+
# Cop supports --auto-correct.
|
19
|
+
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
20
|
+
# URISchemes: http, https
|
21
|
+
Layout/LineLength:
|
22
|
+
Max: 182
|
23
|
+
|
24
|
+
# Offense count: 4
|
25
|
+
# Cop supports --auto-correct.
|
26
|
+
# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator.
|
27
|
+
# SupportedStylesForExponentOperator: space, no_space
|
28
|
+
Layout/SpaceAroundOperators:
|
29
|
+
Exclude:
|
30
|
+
- 'test/graphql_test.rb'
|
31
|
+
|
32
|
+
# Offense count: 6
|
33
|
+
# Cop supports --auto-correct.
|
34
|
+
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
|
35
|
+
# SupportedStyles: space, no_space
|
36
|
+
# SupportedStylesForEmptyBraces: space, no_space
|
37
|
+
Layout/SpaceBeforeBlockBraces:
|
38
|
+
Exclude:
|
39
|
+
- 'test/support/db.rb'
|
40
|
+
|
41
|
+
# Offense count: 5
|
42
|
+
# Cop supports --auto-correct.
|
43
|
+
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
|
44
|
+
# SupportedStyles: space, no_space, compact
|
45
|
+
# SupportedStylesForEmptyBraces: space, no_space
|
46
|
+
Layout/SpaceInsideHashLiteralBraces:
|
47
|
+
Exclude:
|
48
|
+
- 'test/graphql_test.rb'
|
49
|
+
- 'test/multiplex_test.rb'
|
50
|
+
|
51
|
+
# Offense count: 1
|
52
|
+
# Cop supports --auto-correct.
|
53
|
+
# Configuration parameters: EnforcedStyle.
|
54
|
+
# SupportedStyles: space, no_space
|
55
|
+
Layout/SpaceInsideParens:
|
56
|
+
Exclude:
|
57
|
+
- 'test/loader_test.rb'
|
58
|
+
|
59
|
+
# Offense count: 3
|
60
|
+
Lint/MissingSuper:
|
61
|
+
Exclude:
|
62
|
+
- 'test/executor_test.rb'
|
63
|
+
- 'test/loader_test.rb'
|
64
|
+
- 'test/support/loaders.rb'
|
65
|
+
|
66
|
+
# Offense count: 5
|
67
|
+
# Cop supports --auto-correct.
|
68
|
+
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
|
69
|
+
Lint/UnusedBlockArgument:
|
70
|
+
Exclude:
|
71
|
+
- 'lib/graphql/batch.rb'
|
72
|
+
- 'test/support/db.rb'
|
73
|
+
- 'test/support/loaders.rb'
|
74
|
+
- 'test/support/schema.rb'
|
75
|
+
|
76
|
+
# Offense count: 6
|
77
|
+
# Cop supports --auto-correct.
|
78
|
+
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
|
79
|
+
Lint/UnusedMethodArgument:
|
80
|
+
Exclude:
|
81
|
+
- 'lib/graphql/batch/loader.rb'
|
82
|
+
- 'lib/graphql/batch/setup.rb'
|
83
|
+
- 'lib/graphql/batch/setup_multiplex.rb'
|
84
|
+
- 'test/executor_test.rb'
|
85
|
+
|
86
|
+
# Offense count: 2
|
87
|
+
Lint/UselessAssignment:
|
88
|
+
Exclude:
|
89
|
+
- 'test/loader_test.rb'
|
90
|
+
|
91
|
+
# Offense count: 1
|
92
|
+
# Configuration parameters: CountBlocks.
|
93
|
+
Metrics/BlockNesting:
|
94
|
+
Max: 4
|
95
|
+
|
96
|
+
# Offense count: 1
|
97
|
+
# Cop supports --auto-correct.
|
98
|
+
# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, IgnoredMethods, AllowBracesOnProceduralOneLiners.
|
99
|
+
# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
|
100
|
+
# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
|
101
|
+
# FunctionalMethods: let, let!, subject, watch
|
102
|
+
# IgnoredMethods: lambda, proc, it
|
103
|
+
Style/BlockDelimiters:
|
104
|
+
Exclude:
|
105
|
+
- 'test/support/db.rb'
|
106
|
+
|
107
|
+
# Offense count: 11
|
108
|
+
# Cop supports --auto-correct.
|
109
|
+
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
110
|
+
# SupportedStyles: nested, compact
|
111
|
+
Style/ClassAndModuleChildren:
|
112
|
+
Exclude:
|
113
|
+
- 'lib/graphql/batch/executor.rb'
|
114
|
+
- 'lib/graphql/batch/loader.rb'
|
115
|
+
- 'lib/graphql/batch/mutation_field_extension.rb'
|
116
|
+
- 'lib/graphql/batch/setup.rb'
|
117
|
+
- 'lib/graphql/batch/setup_multiplex.rb'
|
118
|
+
- 'test/batch_test.rb'
|
119
|
+
- 'test/custom_executor_test.rb'
|
120
|
+
- 'test/executor_test.rb'
|
121
|
+
- 'test/graphql_test.rb'
|
122
|
+
- 'test/loader_test.rb'
|
123
|
+
- 'test/multiplex_test.rb'
|
124
|
+
|
125
|
+
# Offense count: 1
|
126
|
+
# Cop supports --auto-correct.
|
127
|
+
# Configuration parameters: EnforcedStyle, AllowInnerBackticks.
|
128
|
+
# SupportedStyles: backticks, percent_x, mixed
|
129
|
+
Style/CommandLiteral:
|
130
|
+
Exclude:
|
131
|
+
- 'graphql-batch.gemspec'
|
132
|
+
|
133
|
+
# Offense count: 2
|
134
|
+
# Cop supports --auto-correct.
|
135
|
+
Style/EmptyLiteral:
|
136
|
+
Exclude:
|
137
|
+
- 'test/support/schema.rb'
|
138
|
+
|
139
|
+
# Offense count: 24
|
140
|
+
# Cop supports --auto-correct.
|
141
|
+
# Configuration parameters: EnforcedStyle.
|
142
|
+
# SupportedStyles: always, never
|
143
|
+
Style/FrozenStringLiteralComment:
|
144
|
+
Enabled: false
|
145
|
+
|
146
|
+
# Offense count: 72
|
147
|
+
# Cop supports --auto-correct.
|
148
|
+
# Configuration parameters: IgnoreMacros, IgnoredMethods, IgnoredPatterns, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, EnforcedStyle.
|
149
|
+
# SupportedStyles: require_parentheses, omit_parentheses
|
150
|
+
Style/MethodCallWithArgsParentheses:
|
151
|
+
Exclude:
|
152
|
+
- 'Gemfile'
|
153
|
+
- 'graphql-batch.gemspec'
|
154
|
+
- 'test/batch_test.rb'
|
155
|
+
- 'test/custom_executor_test.rb'
|
156
|
+
- 'test/executor_test.rb'
|
157
|
+
- 'test/graphql_test.rb'
|
158
|
+
- 'test/loader_test.rb'
|
159
|
+
- 'test/multiplex_test.rb'
|
160
|
+
- 'test/support/schema.rb'
|
161
|
+
- 'test/test_helper.rb'
|
162
|
+
|
163
|
+
# Offense count: 1
|
164
|
+
# Cop supports --auto-correct.
|
165
|
+
Style/RedundantBegin:
|
166
|
+
Exclude:
|
167
|
+
- 'lib/graphql/batch.rb'
|
168
|
+
|
169
|
+
# Offense count: 1
|
170
|
+
# Cop supports --auto-correct.
|
171
|
+
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods.
|
172
|
+
# AllowedMethods: present?, blank?, presence, try, try!
|
173
|
+
Style/SafeNavigation:
|
174
|
+
Exclude:
|
175
|
+
- 'test/support/db.rb'
|
176
|
+
|
177
|
+
# Offense count: 1
|
178
|
+
# Cop supports --auto-correct.
|
179
|
+
# Configuration parameters: AllowAsExpressionSeparator.
|
180
|
+
Style/Semicolon:
|
181
|
+
Exclude:
|
182
|
+
- 'test/support/schema.rb'
|
183
|
+
|
184
|
+
# Offense count: 3
|
185
|
+
# Cop supports --auto-correct.
|
186
|
+
# Configuration parameters: EnforcedStyleForMultiline.
|
187
|
+
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
188
|
+
Style/TrailingCommaInArrayLiteral:
|
189
|
+
Exclude:
|
190
|
+
- 'test/graphql_test.rb'
|
191
|
+
|
192
|
+
# Offense count: 23
|
193
|
+
# Cop supports --auto-correct.
|
194
|
+
# Configuration parameters: EnforcedStyleForMultiline.
|
195
|
+
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
196
|
+
Style/TrailingCommaInHashLiteral:
|
197
|
+
Exclude:
|
198
|
+
- 'test/graphql_test.rb'
|
199
|
+
- 'test/multiplex_test.rb'
|
data/Gemfile
CHANGED
@@ -2,6 +2,8 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
gem 'graphql', ENV['GRAPHQL_VERSION'] if ENV['GRAPHQL_VERSION']
|
6
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.4.0')
|
7
|
+
gem 'rubocop', '~> 1.12.0', require: false
|
8
|
+
gem "rubocop-shopify", '~> 1.0.7', require: false
|
7
9
|
end
|
data/README.md
CHANGED
@@ -48,7 +48,9 @@ class RecordLoader < GraphQL::Batch::Loader
|
|
48
48
|
end
|
49
49
|
```
|
50
50
|
|
51
|
-
Use `GraphQL::Batch` as a plugin in your schema
|
51
|
+
Use `GraphQL::Batch` as a plugin in your schema _after_ specifying the mutation
|
52
|
+
so that `GraphQL::Batch` can extend the mutation fields to clear the cache after
|
53
|
+
they are resolved.
|
52
54
|
|
53
55
|
```ruby
|
54
56
|
class MySchema < GraphQL::Schema
|
@@ -59,25 +61,6 @@ class MySchema < GraphQL::Schema
|
|
59
61
|
end
|
60
62
|
```
|
61
63
|
|
62
|
-
For pre `1.5.0` versions:
|
63
|
-
|
64
|
-
```ruby
|
65
|
-
MySchema = GraphQL::Schema.define do
|
66
|
-
query MyQueryType
|
67
|
-
|
68
|
-
GraphQL::Batch.use(self)
|
69
|
-
end
|
70
|
-
```
|
71
|
-
|
72
|
-
##### With `1.9.0`'s `Interpreter` runtime
|
73
|
-
|
74
|
-
Add `GraphQL::Batch` _after_ the interpreter, so that `GraphQL::Batch` can detect the interpreter and attach the right integrations:
|
75
|
-
|
76
|
-
```ruby
|
77
|
-
use GraphQL::Execution::Interpreter
|
78
|
-
use GraphQL::Batch
|
79
|
-
```
|
80
|
-
|
81
64
|
#### Field Usage
|
82
65
|
|
83
66
|
The loader class can be used from the resolver for a graphql field by calling `.for` with the grouping arguments to get a loader instance, then call `.load` on that instance with the key to load.
|
@@ -99,7 +82,7 @@ field :products, [Types::Product, null: true], null: false do
|
|
99
82
|
argument :ids, [ID], required: true
|
100
83
|
end
|
101
84
|
|
102
|
-
def
|
85
|
+
def products(ids:)
|
103
86
|
RecordLoader.for(Product).load_many(ids)
|
104
87
|
end
|
105
88
|
```
|
@@ -150,11 +133,11 @@ end
|
|
150
133
|
```ruby
|
151
134
|
def product(id:)
|
152
135
|
# Try the cache first ...
|
153
|
-
CacheLoader.for(Product).load(
|
136
|
+
CacheLoader.for(Product).load(id).then(nil, lambda do |exc|
|
154
137
|
# But if there's a connection error, go to the underlying database
|
155
138
|
raise exc unless exc.is_a?(Redis::BaseConnectionError)
|
156
139
|
logger.warn err.message
|
157
|
-
RecordLoader.for(Product).load(
|
140
|
+
RecordLoader.for(Product).load(id)
|
158
141
|
end)
|
159
142
|
end
|
160
143
|
```
|
@@ -162,19 +145,19 @@ end
|
|
162
145
|
## Unit Testing
|
163
146
|
|
164
147
|
Your loaders can be tested outside of a GraphQL query by doing the
|
165
|
-
batch loads in a block passed to GraphQL::Batch.batch
|
148
|
+
batch loads in a block passed to `GraphQL::Batch.batch`. That method
|
166
149
|
will set up thread-local state to store the loaders, batch load any
|
167
150
|
promise returned from the block then clear the thread-local state
|
168
151
|
to avoid leaking state between tests.
|
169
152
|
|
170
153
|
```ruby
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
end
|
176
|
-
assert_equal product.title, title
|
154
|
+
def test_single_query
|
155
|
+
product = products(:snowboard)
|
156
|
+
title = GraphQL::Batch.batch do
|
157
|
+
RecordLoader.for(Product).load(product.id).then(&:title)
|
177
158
|
end
|
159
|
+
assert_equal product.title, title
|
160
|
+
end
|
178
161
|
```
|
179
162
|
|
180
163
|
## Development
|
data/Rakefile
CHANGED
@@ -7,4 +7,13 @@ Rake::TestTask.new(:test) do |t|
|
|
7
7
|
t.test_files = FileList['test/**/*_test.rb']
|
8
8
|
end
|
9
9
|
|
10
|
-
task
|
10
|
+
task(default: :test)
|
11
|
+
|
12
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.4.0')
|
13
|
+
task :rubocop do
|
14
|
+
require 'rubocop/rake_task'
|
15
|
+
RuboCop::RakeTask.new
|
16
|
+
end
|
17
|
+
|
18
|
+
task(default: :rubocop)
|
19
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
####
|
2
|
+
# This is a loader for has_one_attached and has_many_attached Active Storage attachments
|
3
|
+
# To load a variant for an attachment, 2 queries are required
|
4
|
+
# Using preloading via the includes method.
|
5
|
+
####
|
6
|
+
|
7
|
+
####
|
8
|
+
# The model with an attached image and many attached pictures
|
9
|
+
####
|
10
|
+
|
11
|
+
# class Event < ApplicationRecord
|
12
|
+
# has_one_attached :image
|
13
|
+
# has_many_attached :pictures
|
14
|
+
# end
|
15
|
+
|
16
|
+
####
|
17
|
+
# An example data type using the AttachmentLoader
|
18
|
+
####
|
19
|
+
|
20
|
+
# class Types::EventType < Types::BaseObject
|
21
|
+
# graphql_name 'Event'
|
22
|
+
#
|
23
|
+
# field :id, ID, null: false
|
24
|
+
# field :image, String, null: true
|
25
|
+
# field :pictures, String, null: true
|
26
|
+
#
|
27
|
+
# def image
|
28
|
+
# AttachmentLoader.for(:Event, :image).load(object.id).then do |image|
|
29
|
+
# Rails.application.routes.url_helpers.url_for(
|
30
|
+
# image.variant({ quality: 75 })
|
31
|
+
# )
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# def pictures
|
36
|
+
# AttachmentLoader.for(:Event, :pictures, association_type: :has_many_attached).load(object.id).then do |pictures|
|
37
|
+
# pictures.map do |picture|
|
38
|
+
# Rails.application.routes.url_helpers.url_for(
|
39
|
+
# picture.variant({ quality: 75 })
|
40
|
+
# )
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
module Loaders
|
46
|
+
class ActiveStorageLoader < GraphQL::Batch::Loader
|
47
|
+
attr_reader :record_type, :attachment_name, :association_type # should be has_one_attached or has_many_attached
|
48
|
+
|
49
|
+
def initialize(record_type, attachment_name, association_type: :has_one_attached)
|
50
|
+
super()
|
51
|
+
@record_type = record_type
|
52
|
+
@attachment_name = attachment_name
|
53
|
+
@association_type = association_type
|
54
|
+
end
|
55
|
+
|
56
|
+
def perform(record_ids)
|
57
|
+
# find records and fulfill promises
|
58
|
+
attachments = ActiveStorage::Attachment.includes(:blob).where(
|
59
|
+
record_type: record_type, record_id: record_ids, name: attachment_name
|
60
|
+
)
|
61
|
+
|
62
|
+
if @association_type == :has_one_attached
|
63
|
+
attachments.each do |attachment|
|
64
|
+
fulfill(attachment.record_id, attachment)
|
65
|
+
end
|
66
|
+
|
67
|
+
record_ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
|
68
|
+
else
|
69
|
+
record_ids.each do |record_id|
|
70
|
+
fulfill(record_id, attachments.select { |attachment| attachment.record_id == record_id })
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -5,6 +5,7 @@ class AssociationLoader < GraphQL::Batch::Loader
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def initialize(model, association_name)
|
8
|
+
super()
|
8
9
|
@model = model
|
9
10
|
@association_name = association_name
|
10
11
|
validate
|
@@ -35,7 +36,7 @@ class AssociationLoader < GraphQL::Batch::Loader
|
|
35
36
|
end
|
36
37
|
|
37
38
|
def preload_association(records)
|
38
|
-
::ActiveRecord::Associations::Preloader.new
|
39
|
+
::ActiveRecord::Associations::Preloader.new(records: records, associations: @association_name).call
|
39
40
|
end
|
40
41
|
|
41
42
|
def read_association(record)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# A sample HTTP loader using:
|
2
|
+
#
|
3
|
+
# 1. https://github.com/httprb/http
|
4
|
+
# 2. https://github.com/mperham/connection_pool
|
5
|
+
# 3. https://github.com/ruby-concurrency/concurrent-ruby
|
6
|
+
#
|
7
|
+
# Setup:
|
8
|
+
#
|
9
|
+
# field :weather, String, null: true do
|
10
|
+
# argument :lat, Float, required: true
|
11
|
+
# argument :lng, Float, required: true
|
12
|
+
# argument :lang, String, required: true
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def weather(lat:, lng:, lang:)
|
16
|
+
# key = Rails.application.credentials.darksky_key
|
17
|
+
# path = "/forecast/#{key}/#{lat},#{lng}?lang=#{lang}"
|
18
|
+
# Loaders::HTTPLoader
|
19
|
+
# .for(host: 'https://api.darksky.net')
|
20
|
+
# .load(->(connection) { connection.get(path).flush })
|
21
|
+
# .then do |response|
|
22
|
+
# if response.status.ok?
|
23
|
+
# json = JSON.parse(response.body)
|
24
|
+
# json['currently']['summary']
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# Querying:
|
30
|
+
#
|
31
|
+
# <<~GQL
|
32
|
+
# query Weather {
|
33
|
+
# montreal: weather(lat: 45.5017, lng: -73.5673, lang: "fr")
|
34
|
+
# waterloo: weather(lat: 43.4643, lng: -80.5204, lang: "en")
|
35
|
+
# }
|
36
|
+
# GQL
|
37
|
+
|
38
|
+
module Loaders
|
39
|
+
class HTTPLoader < GraphQL::Batch::Loader
|
40
|
+
def initialize(host:, size: 4, timeout: 4)
|
41
|
+
super()
|
42
|
+
@host = host
|
43
|
+
@size = size
|
44
|
+
@timeout = timeout
|
45
|
+
end
|
46
|
+
|
47
|
+
def perform(operations)
|
48
|
+
futures = operations.map do |operation|
|
49
|
+
Concurrent::Promises.future do
|
50
|
+
pool.with { |connection| operation.call(connection) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
operations.each_with_index.each do |operation, index|
|
54
|
+
fulfill(operation, futures[index].value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def pool
|
61
|
+
@pool ||= ConnectionPool.new(size: @size, timeout: @timeout) do
|
62
|
+
HTTP.persistent(@host)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/examples/record_loader.rb
CHANGED
@@ -0,0 +1,81 @@
|
|
1
|
+
####
|
2
|
+
# This is a has_many loader which takes advantage of Postgres'
|
3
|
+
# windowing functionality to load the first N records for
|
4
|
+
# a given relationship.
|
5
|
+
####
|
6
|
+
|
7
|
+
####
|
8
|
+
# An example data type using the WindowKeyLoader
|
9
|
+
####
|
10
|
+
|
11
|
+
# class Types::CategoryType < Types::BaseObject
|
12
|
+
# graphql_name 'Category'
|
13
|
+
|
14
|
+
# field :id, ID, null: false
|
15
|
+
# field :events, [Types::EventType], null: false do
|
16
|
+
# argument :first, Int, required: false, default_value: 5
|
17
|
+
# end
|
18
|
+
|
19
|
+
# def events(first:)
|
20
|
+
# WindowKeyLoader.for(
|
21
|
+
# Event,
|
22
|
+
# :category_id,
|
23
|
+
# limit: first, order_col: :start_time, order_dir: :desc
|
24
|
+
# ).load(object.id)
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
|
28
|
+
####
|
29
|
+
# The SQL that is produced
|
30
|
+
####
|
31
|
+
|
32
|
+
# SELECT
|
33
|
+
# "events".*
|
34
|
+
# FROM (
|
35
|
+
# SELECT
|
36
|
+
# "events".*,
|
37
|
+
# row_number() OVER (PARTITION BY category_id ORDER BY start_time DESC) AS rank
|
38
|
+
# FROM
|
39
|
+
# "events"
|
40
|
+
# WHERE
|
41
|
+
# "events"."category_id" IN(1, 2, 3, 4, 5)) AS events
|
42
|
+
# WHERE (rank <= 5)
|
43
|
+
|
44
|
+
class WindowKeyLoader < GraphQL::Batch::Loader
|
45
|
+
attr_reader :model, :foreign_key, :limit, :order_col, :order_dir
|
46
|
+
|
47
|
+
def initialize(model, foreign_key, limit:, order_col:, order_dir: :asc)
|
48
|
+
super()
|
49
|
+
@model = model
|
50
|
+
@foreign_key = foreign_key
|
51
|
+
@limit = limit
|
52
|
+
@order_col = order_col
|
53
|
+
@order_dir = order_dir
|
54
|
+
end
|
55
|
+
|
56
|
+
def perform(foreign_ids)
|
57
|
+
# build the sub-query, limiting results by foreign key at this point
|
58
|
+
# we don't want to execute this query but get its SQL to be used later
|
59
|
+
ranked_from =
|
60
|
+
model.select(
|
61
|
+
"*",
|
62
|
+
"row_number() OVER (
|
63
|
+
PARTITION BY #{foreign_key} ORDER BY #{order_col} #{order_dir}
|
64
|
+
) as rank"
|
65
|
+
).where(foreign_key => foreign_ids).to_sql
|
66
|
+
|
67
|
+
# use the sub-query from above to query records which have a rank
|
68
|
+
# value less than or equal to our limit
|
69
|
+
records =
|
70
|
+
model.from("(#{ranked_from}) as #{model.table_name}").where(
|
71
|
+
"rank <= #{limit}"
|
72
|
+
).to_a
|
73
|
+
|
74
|
+
# match records and fulfill promises
|
75
|
+
foreign_ids.each do |foreign_id|
|
76
|
+
matching_records =
|
77
|
+
records.select { |r| foreign_id == r.send(foreign_key) }
|
78
|
+
fulfill(foreign_id, matching_records)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/graphql-batch.gemspec
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'graphql/batch/version'
|
1
|
+
require_relative 'lib/graphql/batch/version'
|
5
2
|
|
6
3
|
Gem::Specification.new do |spec|
|
7
4
|
spec.name = "graphql-batch"
|
@@ -18,10 +15,12 @@ Gem::Specification.new do |spec|
|
|
18
15
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
16
|
spec.require_paths = ["lib"]
|
20
17
|
|
21
|
-
spec.
|
18
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
19
|
+
|
20
|
+
spec.add_runtime_dependency "graphql", ">= 1.10", "< 2"
|
22
21
|
spec.add_runtime_dependency "promise.rb", "~> 0.7.2"
|
23
22
|
|
24
23
|
spec.add_development_dependency "byebug" if RUBY_ENGINE == 'ruby'
|
25
|
-
spec.add_development_dependency "rake", "
|
24
|
+
spec.add_development_dependency "rake", ">= 12.3.3"
|
26
25
|
spec.add_development_dependency "minitest"
|
27
26
|
end
|
data/lib/graphql/batch/loader.rb
CHANGED
@@ -1,21 +1,20 @@
|
|
1
1
|
module GraphQL::Batch
|
2
2
|
class Loader
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
# Use new argument forwarding syntax if available as an optimization
|
4
|
+
if RUBY_ENGINE && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7")
|
5
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
|
6
|
+
def self.for(...)
|
7
|
+
current_executor.loader(loader_key_for(...)) { new(...) }
|
8
|
+
end
|
9
|
+
RUBY
|
10
|
+
else
|
11
|
+
def self.for(*group_args)
|
12
|
+
current_executor.loader(loader_key_for(*group_args)) { new(*group_args) }
|
12
13
|
end
|
13
|
-
|
14
|
-
executor.loader(loader_key) { new(*group_args) }
|
15
14
|
end
|
16
15
|
|
17
|
-
def self.loader_key_for(*group_args)
|
18
|
-
[self]
|
16
|
+
def self.loader_key_for(*group_args, **group_kwargs)
|
17
|
+
[self, group_kwargs, group_args]
|
19
18
|
end
|
20
19
|
|
21
20
|
def self.load(key)
|
@@ -26,6 +25,23 @@ module GraphQL::Batch
|
|
26
25
|
self.for.load_many(keys)
|
27
26
|
end
|
28
27
|
|
28
|
+
class << self
|
29
|
+
private
|
30
|
+
|
31
|
+
def current_executor
|
32
|
+
executor = Executor.current
|
33
|
+
|
34
|
+
unless executor
|
35
|
+
raise GraphQL::Batch::NoExecutorError, 'Cannot create loader without'\
|
36
|
+
' an Executor. Wrap the call to `for` with `GraphQL::Batch.batch`'\
|
37
|
+
' or use `GraphQL::Batch::SetupMultiplex` as a query instrumenter if'\
|
38
|
+
' using with `graphql-ruby`'
|
39
|
+
end
|
40
|
+
|
41
|
+
executor
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
29
45
|
attr_accessor :loader_key, :executor
|
30
46
|
|
31
47
|
def load(key)
|
@@ -79,7 +95,15 @@ module GraphQL::Batch
|
|
79
95
|
|
80
96
|
# Returns true when the key has already been fulfilled, otherwise returns false
|
81
97
|
def fulfilled?(key)
|
82
|
-
promise_for(key)
|
98
|
+
promise = promise_for(key)
|
99
|
+
# When a promise is fulfilled through this class, it will either:
|
100
|
+
# become fulfilled, if fulfilled with a literal value
|
101
|
+
# become pending with a new source if fulfilled with a promise
|
102
|
+
# Either of these is acceptable, promise.rb will automatically re-wait
|
103
|
+
# on the new source promise as needed.
|
104
|
+
return true if promise.fulfilled?
|
105
|
+
|
106
|
+
promise.pending? && promise.source != self
|
83
107
|
end
|
84
108
|
|
85
109
|
# Must override to load the keys and call #fulfill for each key
|
@@ -124,7 +148,13 @@ module GraphQL::Batch
|
|
124
148
|
|
125
149
|
def check_for_broken_promises(load_keys)
|
126
150
|
load_keys.each do |key|
|
127
|
-
|
151
|
+
promise = promise_for(key)
|
152
|
+
# When a promise is fulfilled through this class, it will either:
|
153
|
+
# become not pending, if fulfilled with a literal value
|
154
|
+
# become pending with a new source if fulfilled with a promise
|
155
|
+
# Either of these is acceptable, promise.rb will automatically re-wait
|
156
|
+
# on the new source promise as needed.
|
157
|
+
next unless promise.pending? && promise.source == self
|
128
158
|
|
129
159
|
reject(key, ::Promise::BrokenError.new("#{self.class} didn't fulfill promise for key #{key.inspect}"))
|
130
160
|
end
|
@@ -6,15 +6,11 @@ module GraphQL::Batch
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def before_multiplex(multiplex)
|
9
|
-
|
9
|
+
GraphQL::Batch::Executor.start_batch(@executor_class)
|
10
10
|
end
|
11
11
|
|
12
12
|
def after_multiplex(multiplex)
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
def instrument(type, field)
|
17
|
-
Setup.instrument_field(@schema, type, field)
|
13
|
+
GraphQL::Batch::Executor.end_batch
|
18
14
|
end
|
19
15
|
end
|
20
16
|
end
|
data/lib/graphql/batch.rb
CHANGED
@@ -16,26 +16,17 @@ module GraphQL
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.use(schema_defn, executor_class: GraphQL::Batch::Executor)
|
19
|
-
|
20
|
-
|
19
|
+
instrumentation = GraphQL::Batch::SetupMultiplex.new(schema_defn, executor_class: executor_class)
|
20
|
+
schema_defn.instrument(:multiplex, instrumentation)
|
21
|
+
|
22
|
+
if schema_defn.mutation
|
21
23
|
require_relative "batch/mutation_field_extension"
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
field.extension(GraphQL::Batch::MutationFieldExtension)
|
26
|
-
end
|
24
|
+
|
25
|
+
schema_defn.mutation.fields.each do |name, field|
|
26
|
+
field.extension(GraphQL::Batch::MutationFieldExtension)
|
27
27
|
end
|
28
|
-
instrumentation = GraphQL::Batch::SetupMultiplex.new(schema, executor_class: executor_class)
|
29
|
-
schema_defn.instrument(:multiplex, instrumentation)
|
30
|
-
elsif GraphQL::VERSION >= "1.6.0"
|
31
|
-
instrumentation = GraphQL::Batch::SetupMultiplex.new(schema, executor_class: executor_class)
|
32
|
-
schema_defn.instrument(:multiplex, instrumentation)
|
33
|
-
schema_defn.instrument(:field, instrumentation)
|
34
|
-
else
|
35
|
-
instrumentation = GraphQL::Batch::Setup.new(schema, executor_class: executor_class)
|
36
|
-
schema_defn.instrument(:query, instrumentation)
|
37
|
-
schema_defn.instrument(:field, instrumentation)
|
38
28
|
end
|
29
|
+
|
39
30
|
schema_defn.lazy_resolve(::Promise, :sync)
|
40
31
|
end
|
41
32
|
end
|
@@ -44,5 +35,4 @@ end
|
|
44
35
|
require_relative "batch/version"
|
45
36
|
require_relative "batch/loader"
|
46
37
|
require_relative "batch/executor"
|
47
|
-
require_relative "batch/setup"
|
48
38
|
require_relative "batch/setup_multiplex"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-batch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dylan Thacker-Smith
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -16,7 +16,7 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.10'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
22
|
version: '2'
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '1.
|
29
|
+
version: '1.10'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '2'
|
@@ -62,16 +62,16 @@ dependencies:
|
|
62
62
|
name: rake
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
|
-
- - "
|
65
|
+
- - ">="
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version:
|
67
|
+
version: 12.3.3
|
68
68
|
type: :development
|
69
69
|
prerelease: false
|
70
70
|
version_requirements: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
|
-
- - "
|
72
|
+
- - ">="
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version:
|
74
|
+
version: 12.3.3
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
76
|
name: minitest
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -93,8 +93,10 @@ executables: []
|
|
93
93
|
extensions: []
|
94
94
|
extra_rdoc_files: []
|
95
95
|
files:
|
96
|
+
- ".github/workflows/ci.yml"
|
96
97
|
- ".gitignore"
|
97
|
-
- ".
|
98
|
+
- ".rubocop.yml"
|
99
|
+
- ".rubocop_todo.yml"
|
98
100
|
- CONTRIBUTING.md
|
99
101
|
- Gemfile
|
100
102
|
- LICENSE.txt
|
@@ -102,20 +104,23 @@ files:
|
|
102
104
|
- Rakefile
|
103
105
|
- bin/console
|
104
106
|
- bin/setup
|
107
|
+
- examples/active_storage_loader.rb
|
105
108
|
- examples/association_loader.rb
|
109
|
+
- examples/http_loader.rb
|
106
110
|
- examples/record_loader.rb
|
111
|
+
- examples/window_key_loader.rb
|
107
112
|
- graphql-batch.gemspec
|
108
113
|
- lib/graphql/batch.rb
|
109
114
|
- lib/graphql/batch/executor.rb
|
110
115
|
- lib/graphql/batch/loader.rb
|
111
116
|
- lib/graphql/batch/mutation_field_extension.rb
|
112
|
-
- lib/graphql/batch/setup.rb
|
113
117
|
- lib/graphql/batch/setup_multiplex.rb
|
114
118
|
- lib/graphql/batch/version.rb
|
115
119
|
homepage: https://github.com/Shopify/graphql-batch
|
116
120
|
licenses:
|
117
121
|
- MIT
|
118
|
-
metadata:
|
122
|
+
metadata:
|
123
|
+
allowed_push_host: https://rubygems.org
|
119
124
|
post_install_message:
|
120
125
|
rdoc_options: []
|
121
126
|
require_paths:
|
@@ -131,8 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
136
|
- !ruby/object:Gem::Version
|
132
137
|
version: '0'
|
133
138
|
requirements: []
|
134
|
-
|
135
|
-
rubygems_version: 2.7.6
|
139
|
+
rubygems_version: 3.2.20
|
136
140
|
signing_key:
|
137
141
|
specification_version: 4
|
138
142
|
summary: A query batching executor for the graphql gem
|
data/.travis.yml
DELETED
data/lib/graphql/batch/setup.rb
DELETED
@@ -1,45 +0,0 @@
|
|
1
|
-
module GraphQL::Batch
|
2
|
-
class Setup
|
3
|
-
class << self
|
4
|
-
def start_batching(executor_class)
|
5
|
-
GraphQL::Batch::Executor.start_batch(executor_class)
|
6
|
-
end
|
7
|
-
|
8
|
-
def end_batching
|
9
|
-
GraphQL::Batch::Executor.end_batch
|
10
|
-
end
|
11
|
-
|
12
|
-
def instrument_field(schema, type, field)
|
13
|
-
return field unless type == schema.mutation
|
14
|
-
old_resolve_proc = field.resolve_proc
|
15
|
-
field.redefine do
|
16
|
-
resolve ->(obj, args, ctx) {
|
17
|
-
GraphQL::Batch::Executor.current.clear
|
18
|
-
begin
|
19
|
-
::Promise.sync(old_resolve_proc.call(obj, args, ctx))
|
20
|
-
ensure
|
21
|
-
GraphQL::Batch::Executor.current.clear
|
22
|
-
end
|
23
|
-
}
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def initialize(schema, executor_class:)
|
29
|
-
@schema = schema
|
30
|
-
@executor_class = executor_class
|
31
|
-
end
|
32
|
-
|
33
|
-
def before_query(query)
|
34
|
-
Setup.start_batching(@executor_class)
|
35
|
-
end
|
36
|
-
|
37
|
-
def after_query(query)
|
38
|
-
Setup.end_batching
|
39
|
-
end
|
40
|
-
|
41
|
-
def instrument(type, field)
|
42
|
-
Setup.instrument_field(@schema, type, field)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|