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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e487e7903d2626761868cfce9a2aa368eeb8efbfa7a930eb00988b80c0ed331
4
- data.tar.gz: cbdff09086c7530c1a0015c3294ed897bb21fd5621fb46a62acb452543d503db
3
+ metadata.gz: bc56d2e745351d747e3bf8d1950761271b0bafea51732bbff4bc88af59f7cc51
4
+ data.tar.gz: 290fea98e4ef1b1f1d1d5ae94a07be35409b314a5c1ce7ca98b38580baffcbd0
5
5
  SHA512:
6
- metadata.gz: 2ced151a4c528db0b2bb29f5be1d8fe6aa4d316344510503682dccb1937760b92953ac1b2825e01d5397c751f618967a85bb9f518951a7127f0525f882412517
7
- data.tar.gz: b8c4a168788b2288955e941e3b5a806a125de2fe52c19fdf58956fba84c30c80e3e7e6e43e79fb285e38e7dbba590616b767d4617a7ec1869ca102d8708e41cb
6
+ metadata.gz: f857c894b252512962d6488bc131b19e3b91b3ea89845d644604bd93aa4503f192673b1e45ef4f2619726d02e740d0e9086eed88c3142d7f2f4ab2cd1e0137b3
7
+ data.tar.gz: 9a10333cf145142d5e57890da59a90f2fc4ea755a2c0e412e8269e531850b62f132e63e5fe0eb5ff064f85c70fc6d26ebea7164badd73ce4177c688b172877e8
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql_preload_queries (0.1.0)
4
+ graphql_preload_queries (0.2.0)
5
5
  graphql
6
6
  rails
7
7
 
data/README.md CHANGED
@@ -1,77 +1,64 @@
1
- # GraphqlPreloadQueries (In progress)
2
- This gem helps to define all possible preloads for graphql data results and avoid the common problem "N+1 Queries".
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
- * Preloads in query results
5
+ * Object Type
6
6
  ```ruby
7
- # queries/articles.rb
8
- def articles
9
- resolve_preloads(Article.all, { allComments: :comments })
10
- end
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
- When articles query is performed and:
13
- * The query includes "allComments", then ```:comments``` will automatically be preloaded
14
- * The query does not include "allComments", then ```:comments``` is not preloaded
17
+ Examples:
18
+ * ```add_preload :friends```
19
+ ```:friends``` association will be preloaded if query includes ```friends```, like: ```user(id: 10) { friends { ... } }```
15
20
 
16
- * Preloads in mutation results
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
- # mutations/articles/approve.rb
19
- def resolve
20
- affected_articles = Article.where(id: [1,2,3])
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
- When approve mutation is performed and:
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 ObjectTypes
48
+ * Preloads in mutation results
30
49
  ```ruby
31
- # types/article_type.rb
32
- module Types
33
- class ArticleType < Types::BaseObject
34
- preload_field :allComments, [Types::CommentType], preload: { owner: :author }, null: false
35
- end
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
- When any query is retrieving an article data and:
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
- # @param key field[:key]
9
- # @param type field[:type]
10
- # @param settings field[:settings] ++ { preload: {} }
11
- # preload: (Hash) { allPosts: [:posts, { author: :author }] }
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
- # Fix: omit non expected "extras" param auto provided by graphql
25
- define_method(key) { |_omit_non_used_args| object.send(key) } unless method_defined? key
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 data (ActiveCollection)
20
- # @return @data with preloads configured
21
- # Sample: resolve_preloads(Category.all, { allPosts: :posts })
22
- def resolve_preloads(data, query_node, preload_config)
23
- apply_preloads(data, filter_preloads(query_node, preload_config))
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.selections.find do |node_i|
43
- key.to_s.split('|').include?(node_i.name.to_s)
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
- filter_preloads(sub_node, preload_conf[1], child_root)
52
- add_preload_key(root, preload_conf[0], child_root.presence || [])
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphqlPreloadQueries
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
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.1.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-27 00:00:00.000000000 Z
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/add_mutation_resolver.rb
129
+ - config/initializers/add_mutation_helper.rb
130
130
  - config/initializers/add_preload_field.rb
131
- - config/initializers/add_query_resolver.rb
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