graphql_preload_queries 0.1.0 → 0.2.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/Gemfile.lock +1 -1
- data/README.md +48 -61
- data/config/initializers/add_mutation_helper.rb +31 -0
- data/config/initializers/add_preload_field.rb +26 -18
- data/config/initializers/add_query_helper.rb +33 -0
- data/config/initializers/patch_continue_value.rb +20 -0
- data/lib/graphql_preload_queries/extensions/preload.rb +23 -23
- data/lib/graphql_preload_queries/version.rb +1 -1
- metadata +5 -4
- data/config/initializers/add_mutation_resolver.rb +0 -26
- data/config/initializers/add_query_resolver.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc56d2e745351d747e3bf8d1950761271b0bafea51732bbff4bc88af59f7cc51
|
4
|
+
data.tar.gz: 290fea98e4ef1b1f1d1d5ae94a07be35409b314a5c1ce7ca98b38580baffcbd0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f857c894b252512962d6488bc131b19e3b91b3ea89845d644604bd93aa4503f192673b1e45ef4f2619726d02e740d0e9086eed88c3142d7f2f4ab2cd1e0137b3
|
7
|
+
data.tar.gz: 9a10333cf145142d5e57890da59a90f2fc4ea755a2c0e412e8269e531850b62f132e63e5fe0eb5ff064f85c70fc6d26ebea7164badd73ce4177c688b172877e8
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,77 +1,64 @@
|
|
1
|
-
# GraphqlPreloadQueries
|
2
|
-
This gem helps to define all
|
1
|
+
# GraphqlPreloadQueries
|
2
|
+
This gem helps you to define all nested preloads to be added when required for graphql data results and avoid the common problem "N+1 Queries".
|
3
3
|
|
4
4
|
## Usage
|
5
|
-
*
|
5
|
+
* Object Type
|
6
6
|
```ruby
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
class UserType < Types::BaseObject
|
8
|
+
add_preload 'parents|allParents', { preload: :parents, friends: :friends, parents: :parents }
|
9
|
+
add_preload :friends, { parents: { preload: :parents, parents: :parents, friends: :friends } }
|
10
|
+
|
11
|
+
field :id, Int, null: true
|
12
|
+
field :name, String, null: true
|
13
|
+
field :friends, [Types::UserType], null: false
|
14
|
+
field :parents, [Types::UserType], null: false
|
15
|
+
end
|
11
16
|
```
|
12
|
-
|
13
|
-
*
|
14
|
-
|
17
|
+
Examples:
|
18
|
+
* ```add_preload :friends```
|
19
|
+
```:friends``` association will be preloaded if query includes ```friends```, like: ```user(id: 10) { friends { ... } }```
|
15
20
|
|
16
|
-
|
21
|
+
* ```add_preload :allFriends, :friends```
|
22
|
+
```:friends``` association will be preloaded if query includes ```allFriends```, like: ```user(id: 10) { allFriends { ... } }```
|
23
|
+
|
24
|
+
* ```add_preload :allFriends, { preload: :friends, parents: :parents }```
|
25
|
+
```:preload``` key can be used to indicate the association name when defining nested preloads, like: ```user(id: 10) { allFriends { id parents { ... } } }```
|
26
|
+
|
27
|
+
* ```add_preload :friends, { allParents: :parents }```
|
28
|
+
(Nested 1 lvl preloading) ```friends: :parents``` association will be preloaded if query includes ```allParents```, like: ```user(id: 10) { friends { allParents { ... } } }```
|
29
|
+
|
30
|
+
* ```add_preload :friends, { allParents: { preload: :parents, friends: :friends } }```
|
31
|
+
(Nested 2 levels preloading) ```friends: { parents: :friends }``` association will be preloaded if query includes ```friends``` inside ```parents```, like: ```user(id: 10) { friends { allParents { { friends { ... } } } } }```
|
32
|
+
|
33
|
+
* ```add_preload 'friends|allFriends', :friends```
|
34
|
+
(Multiple gql queries) ```:friends``` association will be preloaded if query includes ```friends``` or ```allFriends```, like: ```user(id: 10) { friends { ... } }``` OR ```user(id: 10) { allFriends { ... } }```
|
35
|
+
|
36
|
+
* ```add_preload 'ignoredFriends', 'ignored_friends.user'```
|
37
|
+
(Deep preloading) ```{ ignored_friends: :user }``` association will be preloaded if query includes ```inogredFriends```, like: ```user(id: 10) { ignoredFriends { ... } }```
|
38
|
+
|
39
|
+
* Preloads in query results
|
17
40
|
```ruby
|
18
|
-
#
|
19
|
-
def
|
20
|
-
|
21
|
-
res = resolve_preloads(affected_articles, { allComments: :comments })
|
22
|
-
{ articles => res }
|
41
|
+
# queries/users.rb
|
42
|
+
def user(id:)
|
43
|
+
user = include_gql_preloads(:user, User.where(id: id))
|
23
44
|
end
|
24
45
|
```
|
25
|
-
|
26
|
-
* The result articles query includes "allComments", then ```:comments``` will automatically be preloaded
|
27
|
-
* The result articles query does not include "allComments", then ```:comments``` is not preloaded
|
46
|
+
- include_gql_preloads: Will preload all preloads configured in UserType based on the gql query.
|
28
47
|
|
29
|
-
* Preloads in
|
48
|
+
* Preloads in mutation results
|
30
49
|
```ruby
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
50
|
+
# mutations/users/disable.rb
|
51
|
+
#...
|
52
|
+
field :users, [Types::UserType], null: true
|
53
|
+
def resolve(ids:)
|
54
|
+
affected_users = User.where(id: ids)
|
55
|
+
affected_users = include_gql_preloads(:users, affected_users)
|
56
|
+
puts affected_users.first&.friends
|
57
|
+
{ users: affected_users }
|
36
58
|
end
|
37
59
|
```
|
38
|
-
|
39
|
-
* The query includes ```owner``` inside ```allComments```, then ```:author``` will automatically be preloaded inside "allComments" query
|
40
|
-
* The query does not include ```owner```, then ```:author``` is not preloaded
|
41
|
-
This field is exactly the same as the graphql field, except that this field expects for "preload" setting which contains all configurations for preloading
|
60
|
+
- include_gql_preloads: Will preload all preloads configured in UserType based on the gql query.
|
42
61
|
|
43
|
-
Complex preload settings
|
44
|
-
```ruby
|
45
|
-
# category query
|
46
|
-
{
|
47
|
-
'posts' =>
|
48
|
-
[:posts, # :posts preload key will be used when: { posts { id ... } }
|
49
|
-
{
|
50
|
-
'authors|allAuthors' => [:author, { # :author key will be used when: { posts { allAuthors { id ... } } }
|
51
|
-
address: :address # :address key will be used when: { posts { allAuthors { address { id ... } } } }
|
52
|
-
}],
|
53
|
-
history: :versions # :versions key will be used when: { posts { history { ... } } }
|
54
|
-
}
|
55
|
-
],
|
56
|
-
'disabledPosts' => ['category_disabled_posts.post', { # :category_disabled_posts.post key will be used when: { disabledPosts { ... } }
|
57
|
-
authors: :authors # :authors key will be used when: { disabledPosts { authors { ... } } }
|
58
|
-
}]
|
59
|
-
}
|
60
|
-
```
|
61
|
-
* ```authors|allAuthors``` means that the preload will be added if "authors" or "allAuthors" is present in the query
|
62
|
-
* ```category_disabled_posts.post``` means an inner preload, sample: ```posts.preload({ category_disabled_posts: :post })```
|
63
|
-
|
64
|
-
### Important:
|
65
|
-
Is needed to omit "extra" params auto provided by Graphql when using custom resolver (only in case not using params), sample:
|
66
|
-
```ruby
|
67
|
-
# types/post_type.rb
|
68
|
-
preload_field :allComments, [Types::CommentType], preload: { owner: :author }, null: false
|
69
|
-
def allComments(_omit_gql_params) # custom method resolver that omits non used params
|
70
|
-
object.allComments
|
71
|
-
end
|
72
|
-
```
|
73
|
-
|
74
|
-
|
75
62
|
## Installation
|
76
63
|
Add this line to your application's Gemfile:
|
77
64
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# preload resolver for mutations
|
4
|
+
Rails.application.config.to_prepare do
|
5
|
+
GraphQL::Schema::Mutation.class_eval do
|
6
|
+
# TODO: auto recover type_klass using result key
|
7
|
+
# Add corresponding preloads to mutation results
|
8
|
+
# @param gql_result_key (String | Sym)
|
9
|
+
# @param collection (ActiveCollection)
|
10
|
+
# @param type_klass (GQL TypeClass)
|
11
|
+
def include_gql_preloads(gql_result_key, collection, type_klass = nil)
|
12
|
+
type_klass ||= preload_type_klass(gql_result_key.to_s)
|
13
|
+
klass = GraphqlPreloadQueries::Extensions::Preload
|
14
|
+
ast_node = preload_find_node(gql_result_key)
|
15
|
+
klass.preload_associations(collection, ast_node, type_klass)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def preload_find_node(key)
|
21
|
+
main_node = context.query.document.definitions.first.selections.first
|
22
|
+
main_node.selections.find { |node_i| node_i.name == key.to_s }
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param result_key: String
|
26
|
+
def preload_type_klass(result_key)
|
27
|
+
res = self.class.fields[result_key].instance_variable_get(:@return_type_expr)
|
28
|
+
res.is_a?(Array) ? res.first : res
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -3,26 +3,34 @@
|
|
3
3
|
require 'graphql_preload_queries/extensions/preload'
|
4
4
|
|
5
5
|
Rails.application.config.to_prepare do
|
6
|
-
# Custom preload field for Object types
|
7
6
|
Types::BaseObject.class_eval do
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
# ==> <cat1>.preload(posts: :author) // if author and posts are in query
|
13
|
-
# ==> <cat1>.preload(:posts) // if only author is in the query
|
14
|
-
# ==> <cat1>.preload() // if both of them are not in the query
|
15
|
-
# TODO: ability to merge extensions + extras
|
16
|
-
def self.preload_field(key, type, settings = {})
|
17
|
-
klass = GraphqlPreloadQueries::Extensions::Preload
|
18
|
-
custom_attrs = {
|
19
|
-
extras: [:ast_node],
|
20
|
-
extensions: [klass => settings.delete(:preload)]
|
21
|
-
}
|
22
|
-
field key, type, settings.merge(custom_attrs)
|
7
|
+
class << self
|
8
|
+
def preloads
|
9
|
+
@preloads ||= {}
|
10
|
+
end
|
23
11
|
|
24
|
-
#
|
25
|
-
|
12
|
+
# @param key (Symbol|String)
|
13
|
+
# @param preload (Symbol|String or Symbol|String|Hash)
|
14
|
+
# @Sample:
|
15
|
+
## key argument supports for multiple query names
|
16
|
+
# add_preload('users|allUsers', :users)
|
17
|
+
## preload argument indicates the association name to be preloaded
|
18
|
+
# add_preload(:allUsers, :users)
|
19
|
+
## preload argument supports for nested associations
|
20
|
+
# add_preload(:inactiveUsers, 'inactivated_users.user')
|
21
|
+
## "preload" key should be specified to indicate the association name
|
22
|
+
# add_preload(:allUsers, { preload: :users, 'allComments|comments' => :comments } })
|
23
|
+
## preload key can be omitted to use the same name as the key
|
24
|
+
# add_preload(:users, { 'allComments|comments' => :comments } })
|
25
|
+
def add_preload(key, preload)
|
26
|
+
preload ||= key
|
27
|
+
raise('Invalid preload query key') if [String, Symbol].exclude?(key.class)
|
28
|
+
raise('Invalid preload preload key') if [String, Symbol, Hash].exclude?(preload.class)
|
29
|
+
|
30
|
+
preload[:preload] ||= key if preload.is_a?(Hash)
|
31
|
+
key = GraphQL::Schema::Member::BuildType.camelize(key.to_s)
|
32
|
+
preloads[key] = preload
|
33
|
+
end
|
26
34
|
end
|
27
35
|
end
|
28
36
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# preload resolver for queries
|
4
|
+
Rails.application.config.to_prepare do
|
5
|
+
Types::QueryType.class_eval do
|
6
|
+
# TODO: auto recover type_klass using result key
|
7
|
+
# Add corresponding preloads to query results
|
8
|
+
# Note: key is automatically calculated based on method name
|
9
|
+
# @param gql_result_key (String | Sym)
|
10
|
+
# @param collection (ActiveCollection)
|
11
|
+
# @param type_klass (GQL TypeClass, default: calculates using return type)
|
12
|
+
def include_gql_preloads(gql_result_key, collection, type_klass = nil)
|
13
|
+
type_klass ||= preload_type_klass(gql_result_key.to_s)
|
14
|
+
klass = GraphqlPreloadQueries::Extensions::Preload
|
15
|
+
ast_node = preload_find_node(gql_result_key)
|
16
|
+
klass.preload_associations(collection, ast_node, type_klass)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @param key: Symbol
|
22
|
+
def preload_find_node(key)
|
23
|
+
main_node = context.query.document.definitions.first
|
24
|
+
main_node.selections.find { |node_i| node_i.name == key.to_s }
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param result_key: String
|
28
|
+
def preload_type_klass(result_key)
|
29
|
+
res = self.class.fields[result_key].instance_variable_get(:@return_type_expr)
|
30
|
+
res.is_a?(Array) ? res.first : res
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Rails.application.config.to_prepare do
|
4
|
+
GraphQL::Execution::Interpreter::Runtime.class_eval do
|
5
|
+
alias_method :continue_value_old, :continue_value
|
6
|
+
# gql args: path, value, parent_type, field, is_non_null, ast_node
|
7
|
+
def continue_value(*args)
|
8
|
+
value = args[1]
|
9
|
+
ast_node = args[5]
|
10
|
+
field = args[3]
|
11
|
+
type_klass = field.owner
|
12
|
+
if !value.is_a?(ActiveRecord::Relation) || value.loaded? || !type_klass.respond_to?(:preloads)
|
13
|
+
return continue_value_old(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
klass = GraphqlPreloadQueries::Extensions::Preload
|
17
|
+
klass.preload_associations(value, ast_node, type_klass)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -5,51 +5,51 @@
|
|
5
5
|
module GraphqlPreloadQueries
|
6
6
|
module Extensions
|
7
7
|
class Preload < GraphQL::Schema::FieldExtension
|
8
|
-
# extension to add eager loading when a field was already processed
|
9
|
-
def resolve(object:, arguments:, **_rest)
|
10
|
-
klass = GraphqlPreloadQueries::Extensions::Preload
|
11
|
-
res = yield(object, arguments)
|
12
|
-
return res unless res
|
13
|
-
|
14
|
-
klass.resolve_preloads(res, arguments[:ast_node], (options || {}))
|
15
|
-
end
|
16
|
-
|
17
8
|
class << self
|
18
9
|
# Add all the corresponding preloads to the collection
|
19
|
-
# @param
|
20
|
-
# @
|
21
|
-
#
|
22
|
-
|
23
|
-
|
10
|
+
# @param value (ActiveCollection)
|
11
|
+
# @param @node (GqlNode)
|
12
|
+
# @param @type_klass (GqlTypeKlass)
|
13
|
+
# @return @data with necessary preloads
|
14
|
+
def preload_associations(value, node, type_klass)
|
15
|
+
apply_preloads(value, filter_preloads(node, type_klass.preloads || {}))
|
24
16
|
end
|
25
17
|
|
18
|
+
private
|
19
|
+
|
26
20
|
def apply_preloads(collection, preloads)
|
27
21
|
collection.eager_load(preloads)
|
28
22
|
end
|
29
23
|
|
30
24
|
# find all configured preloads inside a node
|
31
25
|
def filter_preloads(node, preload_conf, root = nested_hash)
|
26
|
+
return root unless node
|
27
|
+
|
32
28
|
preload_conf.map do |key, sub_preload_conf|
|
33
29
|
filter_preload(node, key, sub_preload_conf, root)
|
34
30
|
end
|
35
31
|
root
|
36
32
|
end
|
37
33
|
|
38
|
-
private
|
39
|
-
|
40
34
|
# find preloads under a specific key
|
41
35
|
def filter_preload(node, key, preload_conf, root)
|
42
|
-
sub_node = node
|
43
|
-
|
44
|
-
end
|
45
|
-
|
46
|
-
multiple_preload = preload_conf.is_a?(Array)
|
36
|
+
sub_node = sub_node(node, key)
|
37
|
+
multiple_preload = preload_conf.is_a?(Hash)
|
47
38
|
return unless sub_node
|
48
39
|
return add_preload_key(root, preload_conf, []) unless multiple_preload
|
49
40
|
|
50
41
|
child_root = nested_hash
|
51
|
-
|
52
|
-
|
42
|
+
association_name = preload_conf[:preload] || key.to_s.underscore
|
43
|
+
filter_preloads(sub_node, preload_conf, child_root)
|
44
|
+
add_preload_key(root, association_name, child_root.presence || [])
|
45
|
+
end
|
46
|
+
|
47
|
+
def sub_node(node, key)
|
48
|
+
is_relay_node = %w[nodes edges].include?(node.selections.first.name)
|
49
|
+
node = node.selections.first if is_relay_node
|
50
|
+
node.selections.find do |node_i|
|
51
|
+
key.to_s.split('|').include?(node_i.name.to_s)
|
52
|
+
end
|
53
53
|
end
|
54
54
|
|
55
55
|
# parse nested preload key and add it to the tree
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql_preload_queries
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- owen2345
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -126,9 +126,10 @@ files:
|
|
126
126
|
- README.md
|
127
127
|
- Rakefile
|
128
128
|
- bin/rails
|
129
|
-
- config/initializers/
|
129
|
+
- config/initializers/add_mutation_helper.rb
|
130
130
|
- config/initializers/add_preload_field.rb
|
131
|
-
- config/initializers/
|
131
|
+
- config/initializers/add_query_helper.rb
|
132
|
+
- config/initializers/patch_continue_value.rb
|
132
133
|
- gemfiles/Gemfile_4
|
133
134
|
- gemfiles/Gemfile_5
|
134
135
|
- gemfiles/Gemfile_6
|
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# preload resolver for mutations
|
4
|
-
Rails.application.config.to_prepare do
|
5
|
-
GraphQL::Schema::Mutation.class_eval do
|
6
|
-
# Add corresponding preloads to mutation results
|
7
|
-
# @param key (sym) key of the query
|
8
|
-
# @param data (ActiveCollection)
|
9
|
-
# @param preload_config (Same as Field: field[:preload])
|
10
|
-
def resolve_preloads(key, data, preload_config)
|
11
|
-
node = find_node(key)
|
12
|
-
return data unless node
|
13
|
-
|
14
|
-
# relay support (TODO: add support to skip when not using relay)
|
15
|
-
node = node.selections.first if %w[nodes edges].include?(node.selections.first.name)
|
16
|
-
GraphqlPreloadQueries::Extensions::Preload.resolve_preloads(data, node, preload_config)
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def find_node(key)
|
22
|
-
main_node = context.query.document.definitions.first.selections.first
|
23
|
-
main_node.selections.find { |node_i| node_i.name == key.to_s }
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# preload resolver for queries
|
4
|
-
Rails.application.config.to_prepare do
|
5
|
-
Types::QueryType.class_eval do
|
6
|
-
# Add corresponding preloads to query results
|
7
|
-
# Note: key is automatically calculated based on method name
|
8
|
-
# @param data (ActiveCollection)
|
9
|
-
# @param preload_config (Same as Field: field[:preload])
|
10
|
-
def resolve_preloads(data, preload_config)
|
11
|
-
node = find_node(caller[0][/`.*'/][1..-2])
|
12
|
-
return data unless node
|
13
|
-
|
14
|
-
# relay support (TODO: add support to skip when not using relay)
|
15
|
-
node = node.selections.first if %w[nodes edges].include?(node.selections.first.name)
|
16
|
-
GraphqlPreloadQueries::Extensions::Preload.resolve_preloads(data, node, preload_config)
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def find_node(key)
|
22
|
-
main_node = context.query.document.definitions.first
|
23
|
-
main_node.selections.find { |node_i| node_i.name == key.to_s }
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|