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 +4 -4
- data/Gemfile.lock +3 -3
- data/README.md +57 -13
- data/graphql_activerecord_resolvers.gemspec +1 -1
- data/lib/graphql_activerecord_resolvers.rb +3 -3
- data/lib/graphql_activerecord_resolvers/association.rb +63 -0
- data/lib/graphql_activerecord_resolvers/association_tree.rb +21 -0
- data/lib/graphql_activerecord_resolvers/extensions.rb +15 -0
- data/lib/graphql_activerecord_resolvers/version.rb +1 -1
- metadata +7 -6
- data/lib/graphql_activerecord_resolvers/base_resolver.rb +0 -57
- data/lib/graphql_activerecord_resolvers/graphql_association.rb +0 -67
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1453e2c5f560747937e13eac3132dcd5a9d5f392
|
4
|
+
data.tar.gz: 7ca76ca633e8d515ac6127fb2d93beb4759ec8b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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 (
|
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 (~>
|
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
|
28
|
-
|
29
|
-
|
30
|
-
|
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 ->(
|
43
|
-
+ resolve
|
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 ->(
|
50
|
-
+ resolve
|
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
|
-
|
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
|
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
|
-
|
102
|
+
[rubygems.org]: https://rubygems.org
|
63
103
|
|
64
104
|
## Contributing
|
65
105
|
|
66
|
-
Bug reports and pull requests are welcome on GitHub
|
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]
|
112
|
+
The gem is available as open source under the terms of the [MIT License].
|
113
|
+
|
114
|
+
[MIT License]: https://opensource.org/licenses/MIT
|
@@ -1,7 +1,7 @@
|
|
1
|
-
require "graphql_activerecord_resolvers/
|
2
|
-
require "graphql_activerecord_resolvers/
|
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
|
+
)
|
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.
|
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-
|
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: '
|
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: '
|
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/
|
130
|
-
- lib/graphql_activerecord_resolvers/
|
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
|