graphql-sources 0.3.0 → 1.1.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: 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: