graphql_activerecord_resolvers 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f60bace7ac91068af1d90a02898469eab96572bd
4
- data.tar.gz: 0ba1d470d3e34304bb2e007e0ec5c2a8ea40ac5f
3
+ metadata.gz: 1453e2c5f560747937e13eac3132dcd5a9d5f392
4
+ data.tar.gz: 7ca76ca633e8d515ac6127fb2d93beb4759ec8b4
5
5
  SHA512:
6
- metadata.gz: 52137d2b8f781cc2c0c3200766ce6b726c6fc3fca6f7e2e1f21a4a312e3ca867a421654f2a8779abfa2f26a9baca127973813ebe1d854bb2b5a5a02eb62e7f3e
7
- data.tar.gz: 7e3df25124d976177d4d38bd26c9eada577308182c10614c84d5e91de1b29e479d3d304857a12ccdc138fd9312f610357ffad2fae268f64834fa0c55801aaf9f
6
+ metadata.gz: 9c1ce9fb71907a0d8f0771a951e3286c01c4ac62f51cbea00f9b63c4965368a326e766c10155e984f8501ad1e5372d8e957f116714955083683b24eb092f5095
7
+ data.tar.gz: be1efe1931617776c429e9a1074e06092fe263bbf57c74c62cd3c9b51dc59f84d1c07e7cced809ccdf332d6be1cfabea3ab299b05deaa2a5e39f4ff9cf4bce25
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql_activerecord_resolvers (0.1.0)
4
+ graphql_activerecord_resolvers (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -21,7 +21,7 @@ GEM
21
21
  ast (2.3.0)
22
22
  coderay (1.1.2)
23
23
  concurrent-ruby (1.0.5)
24
- graphql (0.19.4)
24
+ graphql (1.7.13)
25
25
  i18n (0.9.5)
26
26
  concurrent-ruby (~> 1.0)
27
27
  method_source (0.9.0)
@@ -54,7 +54,7 @@ PLATFORMS
54
54
  DEPENDENCIES
55
55
  activerecord (~> 5.1)
56
56
  bundler (~> 1.16)
57
- graphql (~> 0.19)
57
+ graphql (~> 1.7)
58
58
  graphql_activerecord_resolvers!
59
59
  minitest (~> 5.0)
60
60
  pry (~> 0.11)
data/README.md CHANGED
@@ -24,10 +24,11 @@ GraphQL marks a new era in API development, one in which the clients dictate wha
24
24
  should deliver. But, due to N+1 queries, using GraphQL with Rails is a pain. That's where this gem
25
25
  comes in.
26
26
 
27
- `graphql_activerecord_resolvers` works with [graphql-ruby](http://graphql-ruby.org/). It allows you
28
- to easily substitute your own resolvers for supercharged ones. These resolvers take a look at the
29
- schema, the query, and the context, and automatically build up an `eager_load` instruction that
30
- Rails understands.
27
+ `graphql_activerecord_resolvers` works with the [graphql] gem. It provides an ActiveRecord scope
28
+ that works in tandem with the GraphQL context to automatically preload the requested associations.
29
+ This takes the database performance burden off of you when writing your GraphQL API.
30
+
31
+ [graphql]: http://graphql-ruby.org
31
32
 
32
33
  To use it, simply make the following change to every **root field** in your Query:
33
34
 
@@ -39,32 +40,75 @@ module Types
39
40
  field :countries do
40
41
  type types[Types::CountryType]
41
42
 
42
- - resolve ->(obj, ctx, args) { Country.all }
43
- + resolve GraphQLActiveRecordResolvers::BaseResolver.resolve(Country)
43
+ - resolve ->(_, _, _) { Country.all }
44
+ + resolve ->(_, _, ctx) { Country.preload_graphql_associations(ctx) }
44
45
  end
45
46
 
46
47
  field :locations do
47
48
  type types[Types::LocationType]
48
49
 
49
- - resolve ->(obj, ctx, args) { Location.all }
50
- + resolve GraphQLActiveRecordResolvers::BaseResolver.resolve(Location)
50
+ - resolve ->(_, _, _) { Location.all }
51
+ + resolve ->(_, _, ctx) { Location.preload_graphql_associations(ctx) }
51
52
  end
52
53
  end
53
54
  end
54
55
  ```
55
56
 
56
- That's all you need to do.
57
+ You'll notice the N+1's disappear.
58
+
59
+ ### When field names don't match association names
60
+
61
+ There's a special case that the resolver can't detect automatically, and that is when you have a
62
+ field that resolves to an association but does not match the name of said association. In this case,
63
+ you need to explicitly declare the association name on the field. For example:
64
+
65
+ ```diff
66
+ class Pet < ActiveRecord::Base
67
+ belongs_to :person
68
+ end
69
+
70
+ # ...
71
+
72
+ module Types
73
+ PetType = GraphQL::ObjectType.define do
74
+ name "Pet"
75
+
76
+ field :owner do
77
+ type Types::PersonType
78
+ + association_name :person
79
+ resolve -> (obj, _, _) { obj.person }
80
+ end
81
+ end
82
+ end
83
+ ```
84
+
85
+ ### What about for fields that return single objects?
86
+
87
+ Research is still underway on this. The difficulty lies in determining how resolvers would need to
88
+ be modified to support eager-loading when requested, but also in such a way that redundant eager
89
+ loading doesn't occur.
57
90
 
58
91
  ## Development
59
92
 
60
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
93
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run
94
+ the tests. You can also run `bin/console` for an interactive prompt that will allow you to
95
+ experiment.
96
+
97
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new
98
+ version, update the version number in `version.rb`, and then run `bundle exec rake release`, which
99
+ will create a git tag for the version, push git commits and tags, and push the `.gem` file to
100
+ [rubygems.org].
61
101
 
62
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
102
+ [rubygems.org]: https://rubygems.org
63
103
 
64
104
  ## Contributing
65
105
 
66
- Bug reports and pull requests are welcome on GitHub at https://github.com/stevenpetryk/graphql_activerecord_resolvers.
106
+ Bug reports and pull requests are welcome [on GitHub].
107
+
108
+ [on GitHub]: https://github.com/stevenpetryk/graphql_activerecord_resolvers
67
109
 
68
110
  ## License
69
111
 
70
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
112
+ The gem is available as open source under the terms of the [MIT License].
113
+
114
+ [MIT License]: https://opensource.org/licenses/MIT
@@ -32,5 +32,5 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "pry", "~> 0.11"
33
33
 
34
34
  spec.add_development_dependency "activerecord", "~> 5.1"
35
- spec.add_development_dependency "graphql", "~> 0.19"
35
+ spec.add_development_dependency "graphql", "~> 1.7"
36
36
  end
@@ -1,7 +1,7 @@
1
- require "graphql_activerecord_resolvers/base_resolver"
2
- require "graphql_activerecord_resolvers/graphql_association"
1
+ require "graphql_activerecord_resolvers/extensions"
2
+ require "graphql_activerecord_resolvers/association"
3
+ require "graphql_activerecord_resolvers/association_tree"
3
4
  require "graphql_activerecord_resolvers/version"
4
5
 
5
6
  module GraphQLActiveRecordResolvers
6
- # Your code goes here...
7
7
  end
@@ -0,0 +1,63 @@
1
+ module GraphQLActiveRecordResolvers
2
+ class Association
3
+ attr_reader :klass, :irep_node, :root
4
+
5
+ def initialize(klass:, irep_node:, root:)
6
+ @klass = klass
7
+ @irep_node = irep_node
8
+ @root = root
9
+ end
10
+
11
+ def build_includes_arguments
12
+ if root
13
+ child_associations.map(&:build_includes_arguments)
14
+ elsif child_associations.any?
15
+ {
16
+ irep_node_association_name => child_associations.map(&:build_includes_arguments),
17
+ }
18
+ else
19
+ irep_node_association_name
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def child_irep_nodes_that_map_to_associations
26
+ irep_node.typed_children.values.first.select do |_, irep_node|
27
+ association_names.include?(irep_node_association_name(irep_node))
28
+ end.values
29
+ end
30
+
31
+ def child_associations
32
+ child_irep_nodes_that_map_to_associations.map do |child_irep_node|
33
+ Association.new(
34
+ klass: klass_for_child_irep_node(child_irep_node),
35
+ irep_node: child_irep_node,
36
+ root: false,
37
+ )
38
+ end
39
+ end
40
+
41
+ def field_for_irep_node(irep_node)
42
+ irep_node.definitions.first
43
+ end
44
+
45
+ def associations
46
+ klass.reflect_on_all_associations
47
+ end
48
+
49
+ def association_names
50
+ associations.map(&:name).map(&:to_s)
51
+ end
52
+
53
+ def irep_node_association_name(irep_node = self.irep_node)
54
+ field = field_for_irep_node(irep_node)
55
+ (field.metadata[:association_name] || field.name).to_s
56
+ end
57
+
58
+ def klass_for_child_irep_node(child_irep_node)
59
+ name = irep_node_association_name(child_irep_node)
60
+ associations.detect { |association| association.name.to_s == name }.klass
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,21 @@
1
+ module GraphQLActiveRecordResolvers
2
+ class AssociationTree
3
+ attr_reader :klass, :irep_node
4
+
5
+ def initialize(klass, ctx)
6
+ @klass = klass
7
+ @irep_node = ctx.irep_node
8
+ end
9
+
10
+ def includes_arguments
11
+ @includes_arguments ||=
12
+ Association.
13
+ new(
14
+ klass: klass,
15
+ irep_node: irep_node,
16
+ root: true,
17
+ ).
18
+ build_includes_arguments
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ require "active_record"
2
+ require "graphql"
3
+
4
+ module ActiveRecord
5
+ class Base
6
+ def self.preload_graphql_associations(ctx)
7
+ association_tree = GraphQLActiveRecordResolvers::IncludesTree.new(self, ctx)
8
+ includes(association_tree.includes_arguments)
9
+ end
10
+ end
11
+ end
12
+
13
+ GraphQL::Field.accepts_definitions(
14
+ association_name: GraphQL::Define.assign_metadata_key(:association_name),
15
+ )
@@ -1,3 +1,3 @@
1
1
  module GraphQLActiveRecordResolvers
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql_activerecord_resolvers
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
  - Steven Petryk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-03-03 00:00:00.000000000 Z
11
+ date: 2018-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0.19'
103
+ version: '1.7'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0.19'
110
+ version: '1.7'
111
111
  description:
112
112
  email:
113
113
  - petryk.steven@gmail.com
@@ -126,8 +126,9 @@ files:
126
126
  - bin/setup
127
127
  - graphql_activerecord_resolvers.gemspec
128
128
  - lib/graphql_activerecord_resolvers.rb
129
- - lib/graphql_activerecord_resolvers/base_resolver.rb
130
- - lib/graphql_activerecord_resolvers/graphql_association.rb
129
+ - lib/graphql_activerecord_resolvers/association.rb
130
+ - lib/graphql_activerecord_resolvers/association_tree.rb
131
+ - lib/graphql_activerecord_resolvers/extensions.rb
131
132
  - lib/graphql_activerecord_resolvers/version.rb
132
133
  homepage: https://github.com/stevenpetryk/graphql_activerecord_resolvers
133
134
  licenses:
@@ -1,57 +0,0 @@
1
- module GraphQLActiveRecordResolvers
2
- class BaseResolver
3
- attr_reader :ctx, :schema, :query
4
-
5
- def self.resolve_collection(klass)
6
- ->(_obj, _args, ctx) do
7
- new(klass, ctx).resolve
8
- end
9
- end
10
-
11
- def initialize(klass, ctx)
12
- @klass = klass
13
- @ctx = ctx
14
- @schema = ctx.schema
15
- @query = ctx.query
16
- end
17
-
18
- def resolve
19
- if includes_tree
20
- klass.includes(includes_tree)
21
- else
22
- klass.all
23
- end
24
- end
25
-
26
- def includes_tree
27
- @includes_tree ||=
28
- GraphQLAssociation.
29
- new(
30
- schema: schema,
31
- klass: klass,
32
- field: root_field,
33
- selections: root_selections,
34
- root: true,
35
- ).
36
- build_includes_tree
37
- end
38
-
39
- private
40
-
41
- def root_field
42
- schema.query.get_field(ctx.key)
43
- end
44
-
45
- def root_selections
46
- ctx.ast_node.selections
47
- end
48
-
49
- def klass
50
- if @klass.is_a? String
51
- @klass.constantize
52
- else
53
- @klass
54
- end
55
- end
56
- end
57
- end
@@ -1,67 +0,0 @@
1
- module GraphQLActiveRecordResolvers
2
- class GraphQLAssociation
3
- attr_reader :klass, :schema, :field, :selections, :root
4
-
5
- def initialize(schema:, klass:, field:, selections:, root:)
6
- @klass = klass
7
- @schema = schema
8
- @field = field
9
- @selections = selections
10
- @root = root
11
- end
12
-
13
- def build_includes_tree
14
- if root
15
- child_associations.map(&:build_includes_tree)
16
- elsif child_associations.any?
17
- {
18
- field.name => child_associations.map(&:build_includes_tree),
19
- }
20
- else
21
- field.name
22
- end
23
- end
24
-
25
- private
26
-
27
- def child_fields
28
- selections.map do |selection|
29
- [selection, schema.get_field(field_type, selection.name)]
30
- end
31
- end
32
-
33
- def child_fields_that_are_also_associations
34
- child_fields.select do |_, field|
35
- association_names.map(&:to_s).include?(field.name.to_s)
36
- end
37
- end
38
-
39
- def child_associations
40
- child_fields_that_are_also_associations.map do |(selection, field)|
41
- GraphQLAssociation.new(
42
- schema: schema,
43
- klass: klass_for_association_name(field.name),
44
- field: field,
45
- selections: selection.selections,
46
- root: false,
47
- )
48
- end
49
- end
50
-
51
- def field_type
52
- field.type.unwrap
53
- end
54
-
55
- def associations
56
- klass.reflect_on_all_associations
57
- end
58
-
59
- def association_names
60
- associations.map(&:name)
61
- end
62
-
63
- def klass_for_association_name(name)
64
- associations.detect { |association| association.name.to_s == name.to_s }.klass
65
- end
66
- end
67
- end