graphql-sources 0.3.0 → 1.1.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: f88238e75afcee4b41463d85c77e4ec70a89afffe18baf5bdaa5a780e153335f
4
- data.tar.gz: eb3a39814d97bf3c06c6ea996cd50dcff262a170b5700edd4019f98c2ff80cad
3
+ metadata.gz: 1e1dff9e9ccdc886ea784a7276e4a6623e6cfca8df8286492b28cc8bd237039b
4
+ data.tar.gz: 87249e51a1a7f4355d399883567a24acc17b11ed081b7a9f87dd8604bb3a6e33
5
5
  SHA512:
6
- metadata.gz: 968c6e5955b5f936c040d862219b890137fed330047670ffda3c7b79ecab1531f976696729e24fb50257acacfff6141296071d84d395e27bedfe757cde5f6f5f
7
- data.tar.gz: 6ec3329d4de24ca2fe71f7c339fa4e6e9d569e4ef6e940f4b1174b2707e8641a72857c4ae1cb30ad53b436a53875616d6a4d254586619d2ebf76bf8a9dc26707
6
+ metadata.gz: 32737d8bff6005b5e41a1c19e8d526ae0ce8e8665902f26983d8210cfe2b28f16fb27cab6b16a03cb35549c86f74637ac1f8462eafec4e28a8c0483392e691e5
7
+ data.tar.gz: 29f417cbdcac1a76846d52110c815bb76816532cd3e82be93fb2637b3176809d0c51e88a9d73d2d1f04a02d4e8e2d3374d423ea62754a8ceb4ef028747367a13
data/README.md CHANGED
@@ -10,6 +10,15 @@ If bundler is not being used to manage dependencies, install the gem by executin
10
10
 
11
11
  $ gem install graphql-sources
12
12
 
13
+ The `GraphQL::Dataloader` plugin must be installed in the schema:
14
+
15
+ ```ruby
16
+ class AppSchema < GraphQL::Schema
17
+ use GraphQL::Dataloader
18
+ # ...
19
+ end
20
+ ```
21
+
13
22
  ## Usage
14
23
 
15
24
  ### Loading `has_one` Associations
@@ -63,7 +72,7 @@ end
63
72
  class UserType < GraphQL::Schema::Object
64
73
  field :comments, [CommentType], null: false
65
74
 
66
- def profile
75
+ def comments
67
76
  dataloader
68
77
  .with(GraphQL::Sources::ActiveRecordCollection, ::Comment, key: :user_id)
69
78
  .load(object.id)
@@ -78,6 +87,46 @@ WHERE "comments"."user_id" IN (...)
78
87
  ORDER BY "comments"."id"
79
88
  ```
80
89
 
90
+ ### Loading `has_one_attached` Associations
91
+
92
+ ```ruby
93
+ class User
94
+ has_one_attached :avatar
95
+ end
96
+ ```
97
+
98
+ ```ruby
99
+ class UserType < GraphQL::Schema::Object
100
+ field :avatar, AttachedType, null: false
101
+
102
+ def avatar
103
+ dataloader
104
+ .with(GraphQL::Sources::ActiveStorageHasOneAttached, :avatar)
105
+ .load(object)
106
+ end
107
+ end
108
+ ```
109
+
110
+ ### Loading `has_many_attached` Associations
111
+
112
+ ```ruby
113
+ class User
114
+ has_many_attached :photos
115
+ end
116
+ ```
117
+
118
+ ```ruby
119
+ class UserType < GraphQL::Schema::Object
120
+ field :photos, [AttachedType], null: false
121
+
122
+ def photos
123
+ dataloader
124
+ .with(GraphQL::Sources::ActiveStorageHasOneAttached, :photos)
125
+ .load(object)
126
+ end
127
+ end
128
+ ```
129
+
81
130
  ### Loading Counts
82
131
 
83
132
  ```ruby
@@ -96,7 +145,7 @@ end
96
145
  class PostType < GraphQL::Schema::Object
97
146
  field :likes, Integer, null: false
98
147
 
99
- def comments
148
+ def likes
100
149
  dataloader
101
150
  .with(GraphQL::Sources::ActiveRecordCount, ::Like, key: :post_id)
102
151
  .load(object.id)
@@ -111,6 +160,26 @@ WHERE "likes"."post_id" IN (1, 2, 3, ...)
111
160
  GROUP BY "likes"."post_id"
112
161
  ```
113
162
 
163
+ ### Loading with `Rails.cache`
164
+
165
+ ```ruby
166
+ class UserType < GraphQL::Schema::Object
167
+ field :location, String, null: false
168
+
169
+ def location
170
+ dataloader
171
+ .with(GraphQL::Sources::RailsCache)
172
+ .load(key: "geocode:#{object.latest_ip}", fallback: -> { Geocode.for(object.latest_ip) })
173
+ end
174
+ end
175
+ ```
176
+
177
+ ## Status
178
+
179
+ [![CircleCI](https://circleci.com/gh/ksylvest/graphql-sources.svg?style=svg)](https://circleci.com/gh/ksylvest/graphql-sources)
180
+ [![Maintainability](https://api.codeclimate.com/v1/badges/bc301cb72712637e67dd/maintainability)](https://codeclimate.com/github/ksylvest/graphql-sources/maintainability)
181
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/bc301cb72712637e67dd/test_coverage)](https://codeclimate.com/github/ksylvest/graphql-sources/test_coverage)
182
+
114
183
  ## License
115
184
 
116
185
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './active_record_base'
4
-
5
3
  module GraphQL
6
4
  module Sources
7
5
  # A class for loading `has_many` style associations.
@@ -31,6 +29,8 @@ module GraphQL
31
29
  # WHERE "comments"."user_id" IN (...)
32
30
  # ORDER BY "comments"."id"
33
31
  class ActiveRecordCollection < ActiveRecordBase
32
+ # @param keys [Array] an array of keys
33
+ # @return [Array] grouped records mirroring the keys
34
34
  def fetch(keys)
35
35
  models = models(keys: keys).order(:id).load_async
36
36
  dataloader.yield
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './active_record_base'
4
-
5
3
  module GraphQL
6
4
  module Sources
7
5
  # A class for loading a count of records.
@@ -31,6 +29,8 @@ module GraphQL
31
29
  # WHERE "likes"."post_id" IN (1, 2, 3, ...)
32
30
  # GROUP BY "likes"."post_id"
33
31
  class ActiveRecordCount < ActiveRecordBase
32
+ # @param keys [Array] an array of keys
33
+ # @return [Array] grouped counts for the keys
34
34
  def fetch(keys)
35
35
  map = models(keys: keys).group(@key).count
36
36
  keys.map { |key| map[key] || 0 }
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './active_record_base'
4
-
5
3
  module GraphQL
6
4
  module Sources
7
5
  # A class for loading `has_one` style associations.
@@ -31,6 +29,8 @@ module GraphQL
31
29
  # WHERE "profiles"."user_id" IN (1, 2, 3, ...)
32
30
  # ORDER BY "profiles"."id"
33
31
  class ActiveRecordObject < ActiveRecordBase
32
+ # @param keys [Array] an array of keys
33
+ # @return [Array] indexed records mirroring the keys
34
34
  def fetch(keys)
35
35
  models = models(keys: keys).order(:id).load_async
36
36
  dataloader.yield
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Sources
5
+ # An abstract class for interacting with active storage.
6
+ class ActiveStorageBase < GraphQL::Dataloader::Source
7
+ # @param name [String] the association name
8
+ def initialize(name)
9
+ super()
10
+ @name = name
11
+ end
12
+
13
+ protected
14
+
15
+ # @param records [Array<ActiveRecord::Base>] a collection of records to load attachments for
16
+ # @return [Array<ActiveStorage::Attachment>] the associated attachments with preloaded blobs
17
+ def attachments(records:)
18
+ ActiveStorage::Attachment
19
+ .preload(:blob)
20
+ .where(record: records)
21
+ .where(name: @name)
22
+ .order(:id)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Sources
5
+ # A class for loading `has_many_attached` style associations.
6
+ #
7
+ # class User
8
+ # has_many_attached :photos
9
+ # end
10
+ #
11
+ # class UserType < GraphQL::Schema::Object
12
+ # field :photos, [AttachedType], null: false
13
+ #
14
+ # def photos
15
+ # dataloader
16
+ # .with(GraphQL::Sources::ActiveStorageHasManyAttached, :photos)
17
+ # .load(object)
18
+ # end
19
+ # end
20
+ #
21
+ # The resulting SQL query is:
22
+ #
23
+ # SELECT "active_storage_attachments".*
24
+ # FROM "active_storage_attachments"
25
+ # WHERE "active_storage_attachments"."name" = 'photos'
26
+ # AND "active_storage_attachments"."record_type" = 'User'
27
+ # AND "active_storage_attachments"."record_id" IN (...)
28
+ class ActiveStorageHasManyAttached < ActiveStorageBase
29
+ # @param records [Array<ActiveRecord::Base>] an array of records
30
+ # @return [Array] grouped attachments mirroring the keys
31
+ def fetch(records)
32
+ attachments = attachments(records: records).load_async
33
+ dataloader.yield
34
+
35
+ map = attachments.group_by { |attachment| [attachment.record_type, attachment.record_id] }
36
+ records.map { |record| map[[record.class.name, record.id]] || [] }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Sources
5
+ # A class for loading `has_one_attached` style associations.
6
+ #
7
+ # class User
8
+ # has_one_attached :photo
9
+ # end
10
+ #
11
+ # class UserType < GraphQL::Schema::Object
12
+ # field :avatar, AttachedType, null: false
13
+ #
14
+ # def avatar
15
+ # dataloader
16
+ # .with(GraphQL::Sources::ActiveStorageHasOneAttached, :avatar)
17
+ # .load(object)
18
+ # end
19
+ # end
20
+ #
21
+ # The resulting SQL query is:
22
+ #
23
+ # SELECT "active_storage_attachments".*
24
+ # FROM "active_storage_attachments"
25
+ # WHERE "active_storage_attachments"."name" = 'avatar'
26
+ # AND "active_storage_attachments"."record_type" = 'User'
27
+ # AND "active_storage_attachments"."record_id" IN (...)
28
+ class ActiveStorageHasOneAttached < ActiveStorageBase
29
+ # @param records [Array<ActiveRecord::Base>] an array of records
30
+ # @return [Array] indexed attachments mirroring the keys
31
+ def fetch(records)
32
+ attachments = attachments(records: records).load_async
33
+ dataloader.yield
34
+
35
+ map = attachments.index_by { |attachment| [attachment.record_type, attachment.record_id] }
36
+ records.map { |record| map[[record.class.name, record.id]] }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Sources
5
+ # A class for loading with Rails.cache.
6
+ #
7
+ # class UserType < GraphQL::Schema::Object
8
+ # field :location, String, null: false
9
+ #
10
+ # def location
11
+ # dataloader
12
+ # .with(GraphQL::Sources::RailsCache)
13
+ # .load(key: "geocode:#{object.latest_ip}", fallback: -> { Geocode.for(object.latest_ip) })
14
+ # end
15
+ # end
16
+ class RailsCache < GraphQL::Dataloader::Source
17
+ # @param operations [Array<Hash>] an array of key and fallback hashes
18
+ def fetch(operations)
19
+ keys = operations.pluck(:key)
20
+ fallbacks = operations.to_h { |operation| [operation[:key], operation[:fallback]] }
21
+ results = Rails.cache.fetch_multi(*keys) { |key| fallbacks[key].call }
22
+ keys.map { |key| results[key] }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Sources
5
- VERSION = '0.3.0'
5
+ VERSION = '1.1.0'
6
6
  end
7
7
  end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'graphql'
4
+ require 'zeitwerk'
4
5
 
5
- require_relative './sources/active_record_count'
6
- require_relative './sources/active_record_collection'
7
- require_relative './sources/active_record_object'
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.push_dir(__dir__, namespace: GraphQL)
8
+ loader.setup
8
9
 
9
10
  module GraphQL
10
11
  # A collection of common GraphQL dataloader classes.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-sources
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-05 00:00:00.000000000 Z
11
+ date: 2022-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: factory_bot
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +164,20 @@ dependencies:
150
164
  - - ">="
151
165
  - !ruby/object:Gem::Version
152
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
153
181
  description: Common loaders for various database or cache operations.
154
182
  email:
155
183
  - kevin@ksylvest.com
@@ -165,6 +193,10 @@ files:
165
193
  - lib/graphql/sources/active_record_collection.rb
166
194
  - lib/graphql/sources/active_record_count.rb
167
195
  - lib/graphql/sources/active_record_object.rb
196
+ - lib/graphql/sources/active_storage_base.rb
197
+ - lib/graphql/sources/active_storage_has_many_attached.rb
198
+ - lib/graphql/sources/active_storage_has_one_attached.rb
199
+ - lib/graphql/sources/rails_cache.rb
168
200
  - lib/graphql/sources/version.rb
169
201
  homepage: https://github.com/ksylvest/graphql-sources
170
202
  licenses: