graphql-batch 0.4.3 → 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: 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