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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1f52d43c7f189fab24b5e00f8b34435e99c03ae23ba4c99dfaa7a94d67271ed
4
- data.tar.gz: 0ceb7b68519b2e1bd0f94686376d26590cebb9697fd50e80272d0770a85c4784
3
+ metadata.gz: b257f38787895a689caec957a242bef5fa56e02030b2ac499078a510820b8669
4
+ data.tar.gz: 9232d5995209ff370617a4f68f31cf936fd334ccd9eaad1cc449336d353a6535
5
5
  SHA512:
6
- metadata.gz: '091fe19e752f10751216374384346e2c4bb0d8fbe35e86248b3584396531ff2775b4732380da5109968b402320ca22e0cc2f1f772ca26f9d12b4b2bfc5a5a049'
7
- data.tar.gz: 6890562c5b66004bf10b886ba6370cf7c544ab796f2e29ac279b49c93003079b1722c068155a168f5ee9f3194b34395a2d26ac2e8e3013cecd6e74db2286d8ad
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
@@ -0,0 +1,11 @@
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
+
4
+ inherit_from:
5
+ - .rubocop_todo.yml
6
+
7
+ AllCops:
8
+ SuggestExtensions: false
9
+ TargetRubyVersion: 2.7
10
+ Exclude:
11
+ - vendor/**/*
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
- if ENV["TESTING_INTERPRETER"] == "true"
6
- gem "graphql", "1.9.0.pre4"
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 (for graphql >= `1.5.0`).
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 product(ids:)
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(args["id"]).then(nil, lambda do |exc|
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(args["id"])
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. That method
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
- def test_single_query
172
- product = products(:snowboard)
173
- title = GraphQL::Batch.batch do
174
- RecordLoader.for(Product).load(product.id).then(&:title)
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 :default => :test
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.preload(records, @association_name)
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
@@ -1,5 +1,6 @@
1
1
  class RecordLoader < GraphQL::Batch::Loader
2
2
  def initialize(model, column: model.primary_key, where: nil)
3
+ super()
3
4
  @model = model
4
5
  @column = column.to_s
5
6
  @column_type = model.type_for_attribute(@column)
@@ -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
@@ -1,7 +1,4 @@
1
- # coding: utf-8
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.add_runtime_dependency "graphql", ">= 1.3", "< 2"
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", "~> 10.0"
24
+ spec.add_development_dependency "rake", ">= 12.3.3"
26
25
  spec.add_development_dependency "minitest"
27
26
  end
@@ -1,21 +1,20 @@
1
1
  module GraphQL::Batch
2
2
  class Loader
3
- def self.for(*group_args)
4
- loader_key = loader_key_for(*group_args)
5
- executor = Executor.current
6
-
7
- unless executor
8
- raise GraphQL::Batch::NoExecutorError, 'Cannot create loader without'\
9
- ' an Executor. Wrap the call to `for` with `GraphQL::Batch.batch`'\
10
- ' or use `GraphQL::Batch::Setup` as a query instrumenter if'\
11
- ' using with `graphql-ruby`'
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].concat(group_args)
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).fulfilled?
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
- next unless promise_for(key).pending?
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
- Setup.start_batching(@executor_class)
9
+ GraphQL::Batch::Executor.start_batch(@executor_class)
10
10
  end
11
11
 
12
12
  def after_multiplex(multiplex)
13
- Setup.end_batching
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
@@ -1,5 +1,5 @@
1
1
  module GraphQL
2
2
  module Batch
3
- VERSION = "0.4.0"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  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
- schema = schema_defn.target
20
- if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.9.0.pre3')
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
- if schema.mutation
23
- schema.mutation.fields.each do |name, f|
24
- field = f.metadata[:type_class]
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.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: 2019-02-13 00:00:00.000000000 Z
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.3'
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.3'
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: '10.0'
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: '10.0'
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
- - ".travis.yml"
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
- rubyforge_project:
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
@@ -1,7 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.3
4
- - 2.6
5
- env:
6
- - TESTING_INTERPRETER=true
7
- - TESTING_INTERPRETER=false
@@ -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