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 +4 -4
- data/README.md +71 -2
- data/lib/graphql/sources/active_record_collection.rb +2 -2
- data/lib/graphql/sources/active_record_count.rb +2 -2
- data/lib/graphql/sources/active_record_object.rb +2 -2
- data/lib/graphql/sources/active_storage_base.rb +26 -0
- data/lib/graphql/sources/active_storage_has_many_attached.rb +40 -0
- data/lib/graphql/sources/active_storage_has_one_attached.rb +40 -0
- data/lib/graphql/sources/rails_cache.rb +26 -0
- data/lib/graphql/sources/version.rb +1 -1
- data/lib/graphql/sources.rb +4 -3
- metadata +34 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1e1dff9e9ccdc886ea784a7276e4a6623e6cfca8df8286492b28cc8bd237039b
|
4
|
+
data.tar.gz: 87249e51a1a7f4355d399883567a24acc17b11ed081b7a9f87dd8604bb3a6e33
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
+
[](https://circleci.com/gh/ksylvest/graphql-sources)
|
180
|
+
[](https://codeclimate.com/github/ksylvest/graphql-sources/maintainability)
|
181
|
+
[](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
|
data/lib/graphql/sources.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'graphql'
|
4
|
+
require 'zeitwerk'
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
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:
|
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-
|
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:
|