graphql-batch 0.4.3 → 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: fccdeee0358f45c6750102fa77b7537f11d4a269a40985be5e8c01db1314b346
4
- data.tar.gz: ca0737882a1d16189c262e30e113deffb46d4ed50faabca99f3b81f2c317ed9a
3
+ metadata.gz: b257f38787895a689caec957a242bef5fa56e02030b2ac499078a510820b8669
4
+ data.tar.gz: 9232d5995209ff370617a4f68f31cf936fd334ccd9eaad1cc449336d353a6535
5
5
  SHA512:
6
- metadata.gz: 8edbbe8673b1581b2927fed0ed4af9370f7009d0662cac952bebe0cc00cec2bf32fafe4eee092af47666e61f3f8a09ec3298d95715a7d6436e91af4dacfc2f02
7
- data.tar.gz: 3b4cd32b2f65e7af83394a151df55f9880b8d0381d687c1135dfb541a9dd90e4bdaca8071d5aae68976ae176e3bc74304ae639f884dcad3e26249abd4e4c1334
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 CHANGED
@@ -1,6 +1,11 @@
1
+ inherit_gem:
2
+ rubocop-shopify: rubocop.yml
3
+
1
4
  inherit_from:
2
- - https://shopify.github.io/ruby-style-guide/rubocop.yml
3
5
  - .rubocop_todo.yml
4
6
 
5
7
  AllCops:
6
- TargetRubyVersion: 2.3
8
+ SuggestExtensions: false
9
+ TargetRubyVersion: 2.7
10
+ Exclude:
11
+ - vendor/**/*
data/.rubocop_todo.yml CHANGED
@@ -56,6 +56,13 @@ Layout/SpaceInsideParens:
56
56
  Exclude:
57
57
  - 'test/loader_test.rb'
58
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
+
59
66
  # Offense count: 5
60
67
  # Cop supports --auto-correct.
61
68
  # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
data/Gemfile CHANGED
@@ -3,4 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  gem 'graphql', ENV['GRAPHQL_VERSION'] if ENV['GRAPHQL_VERSION']
6
- gem 'rubocop', '~> 0.78.0', require: false
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
9
+ end
data/README.md CHANGED
@@ -50,7 +50,7 @@ end
50
50
 
51
51
  Use `GraphQL::Batch` as a plugin in your schema _after_ specifying the mutation
52
52
  so that `GraphQL::Batch` can extend the mutation fields to clear the cache after
53
- they are resolved (for graphql >= `1.5.0`).
53
+ they are resolved.
54
54
 
55
55
  ```ruby
56
56
  class MySchema < GraphQL::Schema
@@ -61,16 +61,6 @@ class MySchema < GraphQL::Schema
61
61
  end
62
62
  ```
63
63
 
64
- For pre `1.5.0` versions:
65
-
66
- ```ruby
67
- MySchema = GraphQL::Schema.define do
68
- query MyQueryType
69
-
70
- GraphQL::Batch.use(self)
71
- end
72
- ```
73
-
74
64
  #### Field Usage
75
65
 
76
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.
@@ -155,19 +145,19 @@ end
155
145
  ## Unit Testing
156
146
 
157
147
  Your loaders can be tested outside of a GraphQL query by doing the
158
- 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
159
149
  will set up thread-local state to store the loaders, batch load any
160
150
  promise returned from the block then clear the thread-local state
161
151
  to avoid leaking state between tests.
162
152
 
163
153
  ```ruby
164
- def test_single_query
165
- product = products(:snowboard)
166
- title = GraphQL::Batch.batch do
167
- RecordLoader.for(Product).load(product.id).then(&:title)
168
- end
169
- 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)
170
158
  end
159
+ assert_equal product.title, title
160
+ end
171
161
  ```
172
162
 
173
163
  ## Development
data/Rakefile CHANGED
@@ -7,9 +7,13 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
- task :rubocop do
11
- require 'rubocop/rake_task'
12
- RuboCop::RakeTask.new
13
- end
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
14
17
 
15
- task(default: [:test, :rubocop])
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)
@@ -38,6 +38,7 @@
38
38
  module Loaders
39
39
  class HTTPLoader < GraphQL::Batch::Loader
40
40
  def initialize(host:, size: 4, timeout: 4)
41
+ super()
41
42
  @host = host
42
43
  @size = size
43
44
  @timeout = timeout
@@ -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"
@@ -20,7 +17,7 @@ Gem::Specification.new do |spec|
20
17
 
21
18
  spec.metadata['allowed_push_host'] = "https://rubygems.org"
22
19
 
23
- spec.add_runtime_dependency "graphql", ">= 1.3", "< 2"
20
+ spec.add_runtime_dependency "graphql", ">= 1.10", "< 2"
24
21
  spec.add_runtime_dependency "promise.rb", "~> 0.7.2"
25
22
 
26
23
  spec.add_development_dependency "byebug" if RUBY_ENGINE == 'ruby'
@@ -34,7 +34,7 @@ module GraphQL::Batch
34
34
  unless executor
35
35
  raise GraphQL::Batch::NoExecutorError, 'Cannot create loader without'\
36
36
  ' an Executor. Wrap the call to `for` with `GraphQL::Batch.batch`'\
37
- ' or use `GraphQL::Batch::Setup` as a query instrumenter if'\
37
+ ' or use `GraphQL::Batch::SetupMultiplex` as a query instrumenter if'\
38
38
  ' using with `graphql-ruby`'
39
39
  end
40
40
 
@@ -95,7 +95,15 @@ module GraphQL::Batch
95
95
 
96
96
  # Returns true when the key has already been fulfilled, otherwise returns false
97
97
  def fulfilled?(key)
98
- 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
99
107
  end
100
108
 
101
109
  # Must override to load the keys and call #fulfill for each key
@@ -140,7 +148,13 @@ module GraphQL::Batch
140
148
 
141
149
  def check_for_broken_promises(load_keys)
142
150
  load_keys.each do |key|
143
- 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
144
158
 
145
159
  reject(key, ::Promise::BrokenError.new("#{self.class} didn't fulfill promise for key #{key.inspect}"))
146
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.3"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  end
data/lib/graphql/batch.rb CHANGED
@@ -16,29 +16,17 @@ module GraphQL
16
16
  end
17
17
 
18
18
  def self.use(schema_defn, executor_class: GraphQL::Batch::Executor)
19
- # Support 1.10+ which passes the class instead of the definition proxy
20
- schema = schema_defn.is_a?(Class) ? schema_defn : schema_defn.target
21
- current_gem_version = Gem::Version.new(GraphQL::VERSION)
22
- if current_gem_version >= Gem::Version.new("1.6.0")
23
- instrumentation = GraphQL::Batch::SetupMultiplex.new(schema, executor_class: executor_class)
24
- schema_defn.instrument(:multiplex, instrumentation)
25
- if schema.mutation
26
- if current_gem_version >= Gem::Version.new('1.9.0.pre3') &&
27
- (schema.mutation.is_a?(Class) || schema.mutation.metadata[:type_class])
28
- require_relative "batch/mutation_field_extension"
29
- schema.mutation.fields.each do |name, f|
30
- field = f.respond_to?(:type_class) ? f.type_class : f.metadata[:type_class]
31
- field.extension(GraphQL::Batch::MutationFieldExtension)
32
- end
33
- else
34
- schema_defn.instrument(:field, instrumentation)
35
- end
19
+ instrumentation = GraphQL::Batch::SetupMultiplex.new(schema_defn, executor_class: executor_class)
20
+ schema_defn.instrument(:multiplex, instrumentation)
21
+
22
+ if schema_defn.mutation
23
+ require_relative "batch/mutation_field_extension"
24
+
25
+ schema_defn.mutation.fields.each do |name, field|
26
+ field.extension(GraphQL::Batch::MutationFieldExtension)
36
27
  end
37
- else
38
- instrumentation = GraphQL::Batch::Setup.new(schema, executor_class: executor_class)
39
- schema_defn.instrument(:query, instrumentation)
40
- schema_defn.instrument(:field, instrumentation)
41
28
  end
29
+
42
30
  schema_defn.lazy_resolve(::Promise, :sync)
43
31
  end
44
32
  end
@@ -47,5 +35,4 @@ end
47
35
  require_relative "batch/version"
48
36
  require_relative "batch/loader"
49
37
  require_relative "batch/executor"
50
- require_relative "batch/setup"
51
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.3
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: 2020-05-11 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'
@@ -93,11 +93,10 @@ executables: []
93
93
  extensions: []
94
94
  extra_rdoc_files: []
95
95
  files:
96
+ - ".github/workflows/ci.yml"
96
97
  - ".gitignore"
97
- - ".rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml"
98
98
  - ".rubocop.yml"
99
99
  - ".rubocop_todo.yml"
100
- - ".travis.yml"
101
100
  - CONTRIBUTING.md
102
101
  - Gemfile
103
102
  - LICENSE.txt
@@ -105,15 +104,16 @@ files:
105
104
  - Rakefile
106
105
  - bin/console
107
106
  - bin/setup
107
+ - examples/active_storage_loader.rb
108
108
  - examples/association_loader.rb
109
109
  - examples/http_loader.rb
110
110
  - examples/record_loader.rb
111
+ - examples/window_key_loader.rb
111
112
  - graphql-batch.gemspec
112
113
  - lib/graphql/batch.rb
113
114
  - lib/graphql/batch/executor.rb
114
115
  - lib/graphql/batch/loader.rb
115
116
  - lib/graphql/batch/mutation_field_extension.rb
116
- - lib/graphql/batch/setup.rb
117
117
  - lib/graphql/batch/setup_multiplex.rb
118
118
  - lib/graphql/batch/version.rb
119
119
  homepage: https://github.com/Shopify/graphql-batch
@@ -136,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
136
  - !ruby/object:Gem::Version
137
137
  version: '0'
138
138
  requirements: []
139
- rubygems_version: 3.0.3
139
+ rubygems_version: 3.2.20
140
140
  signing_key:
141
141
  specification_version: 4
142
142
  summary: A query batching executor for the graphql gem