graphql_preload_queries 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|