graphql_preload_queries 0.1.0 → 0.3.1

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: 89226168aa382eb59c56754b96a4eaa64c51c5f06fc6fd8d831b7795d84adf0e
4
+ data.tar.gz: b29ec85aa619a1f7412944d6f50388f68c9b196cc5e8607bc22fd163f3a64ed1
5
5
  SHA512:
6
- metadata.gz: 2ced151a4c528db0b2bb29f5be1d8fe6aa4d316344510503682dccb1937760b92953ac1b2825e01d5397c751f618967a85bb9f518951a7127f0525f882412517
7
- data.tar.gz: b8c4a168788b2288955e941e3b5a806a125de2fe52c19fdf58956fba84c30c80e3e7e6e43e79fb285e38e7dbba590616b767d4617a7ec1869ca102d8708e41cb
6
+ metadata.gz: 42df9aacf5b59d341fc5498a61890a1ecd5c76713179c94b81696084af7d9d4f52abdd105c26a04775aa6b8199f86398841bf230127df711ffceef386752f7f9
7
+ data.tar.gz: cded97146bc935064f7dcaa94034deffd06adf1b6cf3c2daaacf746fb11eef542b261debd505ccb8963ec3b67f60cf7386d4829aeaa13feaed024786fa7ec04e
@@ -0,0 +1,20 @@
1
+ ## 0.3.1 (22-01-2021)
2
+ - feat: auto camelize key for queries and mutations
3
+
4
+ ## 0.3 (22-01-2021)
5
+ - feat: add debug mode
6
+ ```GraphqlPreloadQueries::DEBUG = true```
7
+ - fix: detect the correct Query Result Type
8
+
9
+ ## 0.2.2 (21-01-2021)
10
+ - Fix: Fix deep recursive stack error
11
+
12
+ ## 0.2.1 (21-01-2021)
13
+ - fix: add default preload to key
14
+ - fix: fix invalid key when deep preloading
15
+
16
+ ## 0.2.0 (02-12-2020)
17
+ - Refactor: Preload associations when iterating activeRecord::Relation
18
+
19
+ ## 0.1.0 (10-11-2020
20
+ - Add rails query preload support for queries, mutations and gql object types.
data/Gemfile CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ruby '2.6.5'
3
4
  source 'https://rubygems.org'
4
5
  git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5
6
 
@@ -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.3.1)
5
5
  graphql
6
6
  rails
7
7
 
@@ -190,5 +190,8 @@ DEPENDENCIES
190
190
  rubocop-rspec
191
191
  sqlite3
192
192
 
193
+ RUBY VERSION
194
+ ruby 2.6.5p114
195
+
193
196
  BUNDLED WITH
194
197
  2.1.4
data/README.md CHANGED
@@ -1,77 +1,72 @@
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
+ # includes all preloads defined in user type
44
+ # Sample: user(id: 10){ friends { id } }
45
+ # :friends will be preloaded inside "user" sql query
46
+ user = include_gql_preloads(:user, User.where(id: id))
47
+
48
+ # does not include user type preloads (only sub query preloads will be applied)
49
+ # Sample: user(id: 10){ friends { id parents { ... } } }
50
+ # Only :parents will be preloaded inside "friends" sql query
51
+ user = User.find(id)
23
52
  end
24
53
  ```
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
54
+ - include_gql_preloads: Will preload all preloads configured in UserType based on the gql query.
28
55
 
29
- * Preloads in ObjectTypes
56
+ * Preloads in mutation results
30
57
  ```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
58
+ # mutations/users/disable.rb
59
+ #...
60
+ field :users, [Types::UserType], null: true
61
+ def resolve(ids:)
62
+ affected_users = User.where(id: ids)
63
+ affected_users = include_gql_preloads(:users, affected_users)
64
+ puts affected_users.first&.friends # will print preloaded friends data
65
+ { users: affected_users }
36
66
  end
37
67
  ```
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
42
-
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
- ```
68
+ - include_gql_preloads: Will preload all preloads configured in UserType based on the gql query.
73
69
 
74
-
75
70
  ## Installation
76
71
  Add this line to your application's Gemfile:
77
72
 
@@ -89,6 +84,12 @@ Or install it yourself as:
89
84
  $ gem install graphql_preload_queries
90
85
  ```
91
86
 
87
+ For debugging mode:
88
+ ```
89
+ # config/initializers/gql_preload.rb
90
+ GraphqlPreloadQueries::DEBUG = true
91
+ ```
92
+
92
93
  ## Contributing
93
94
  Bug reports and pull requests are welcome on GitHub at https://github.com/owen2345/graphql_preload_queries. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
94
95
 
@@ -0,0 +1,32 @@
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
+ gql_result_key = GraphQL::Schema::Member::BuildType.camelize(gql_result_key.to_s)
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
+ def preload_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
+
26
+ # @param result_key: String
27
+ def preload_type_klass(result_key)
28
+ res = self.class.fields[result_key].instance_variable_get(:@return_type_expr)
29
+ res.is_a?(Array) ? res.first : res
30
+ end
31
+ end
32
+ 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 = key)
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,34 @@
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
+ gql_result_key = GraphQL::Schema::Member::BuildType.camelize(gql_result_key.to_s)
14
+ type_klass ||= preload_type_klass(gql_result_key.to_s)
15
+ klass = GraphqlPreloadQueries::Extensions::Preload
16
+ ast_node = preload_find_node(gql_result_key)
17
+ klass.preload_associations(collection, ast_node, type_klass)
18
+ end
19
+
20
+ private
21
+
22
+ # @param key: Symbol
23
+ def preload_find_node(key)
24
+ main_node = context.query.document.definitions.first
25
+ main_node.selections.find { |node_i| node_i.name == key.to_s }
26
+ end
27
+
28
+ # @param result_key: String
29
+ def preload_type_klass(result_key)
30
+ res = self.class.fields[result_key].instance_variable_get(:@return_type_expr)
31
+ res.is_a?(Array) ? res.first : res
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql/execution/interpreter/runtime'
4
+ module GraphqlPreloadQueries::PatchContinueValue # rubocop:disable Style/ClassAndModuleChildren:
5
+ # gql args: path, value, parent_type, field, is_non_null, ast_node
6
+ def continue_value(*args)
7
+ value = args[1]
8
+ ast_node = args[5]
9
+ field = args[3]
10
+ type_klass = Array(field.instance_variable_get(:@return_type_expr))[0]
11
+ is_active_record = value.is_a?(ActiveRecord::Relation)
12
+ return super if !is_active_record || value.loaded? || !type_klass.respond_to?(:preloads)
13
+
14
+ klass = GraphqlPreloadQueries::Extensions::Preload
15
+ klass.preload_associations(value, ast_node, type_klass)
16
+ end
17
+ end
18
+ GraphQL::Execution::Interpreter::Runtime.prepend GraphqlPreloadQueries::PatchContinueValue
@@ -5,4 +5,8 @@ require 'graphql'
5
5
  require 'graphql_preload_queries/extensions/preload'
6
6
 
7
7
  module GraphqlPreloadQueries
8
+ DEBUG = false
9
+ def self.log(msg)
10
+ puts "***GraphqlPreloadQueries: #{msg}" if DEBUG
11
+ end
8
12
  end
@@ -5,51 +5,54 @@
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
+ preloads = filter_preloads(node, type_klass.preloads || {})
16
+ log_info = { type_klass: type_klass, preloads: preloads, configured: type_klass.preloads }
17
+ GraphqlPreloadQueries.log("Preloading: #{log_info}")
18
+ apply_preloads(value, preloads)
24
19
  end
25
20
 
21
+ private
22
+
26
23
  def apply_preloads(collection, preloads)
27
24
  collection.eager_load(preloads)
28
25
  end
29
26
 
30
27
  # find all configured preloads inside a node
31
28
  def filter_preloads(node, preload_conf, root = nested_hash)
29
+ return root unless node
30
+
32
31
  preload_conf.map do |key, sub_preload_conf|
33
32
  filter_preload(node, key, sub_preload_conf, root)
34
33
  end
35
34
  root
36
35
  end
37
36
 
38
- private
39
-
40
37
  # find preloads under a specific key
41
38
  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)
39
+ sub_node = sub_node(node, key)
40
+ multiple_preload = preload_conf.is_a?(Hash)
47
41
  return unless sub_node
48
- return add_preload_key(root, preload_conf, []) unless multiple_preload
42
+ return add_preload_key(root, preload_conf, {}) unless multiple_preload
49
43
 
50
44
  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 || [])
45
+ association_name = preload_conf[:preload] || key.to_s.underscore
46
+ filter_preloads(sub_node, preload_conf, child_root)
47
+ add_preload_key(root, association_name, child_root.presence || {})
48
+ end
49
+
50
+ def sub_node(node, key)
51
+ is_relay_node = %w[nodes edges].include?(node.selections.first.name)
52
+ node = node.selections.first if is_relay_node
53
+ node.selections.find do |node_i|
54
+ key.to_s.split('|').include?(node_i.name.to_s)
55
+ end
53
56
  end
54
57
 
55
58
  # 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.3.1'
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.3.1
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: 2021-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -120,15 +120,17 @@ files:
120
120
  - ".gitignore"
121
121
  - ".rspec"
122
122
  - ".rubocop.yml"
123
+ - CHANGELOG.md
123
124
  - Gemfile
124
125
  - Gemfile.lock
125
126
  - MIT-LICENSE
126
127
  - README.md
127
128
  - Rakefile
128
129
  - bin/rails
129
- - config/initializers/add_mutation_resolver.rb
130
+ - config/initializers/add_mutation_helper.rb
130
131
  - config/initializers/add_preload_field.rb
131
- - config/initializers/add_query_resolver.rb
132
+ - config/initializers/add_query_helper.rb
133
+ - config/initializers/patch_continue_value.rb
132
134
  - gemfiles/Gemfile_4
133
135
  - gemfiles/Gemfile_5
134
136
  - 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