graphql-batch 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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