graphql_activerecord_resolvers 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
  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