graphql-groups 0.1.2 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +23 -21
- data/README.md +39 -24
- data/graphql-groups.gemspec +9 -4
- data/lib/graphql/groups/group_type_registry.rb +6 -0
- data/lib/graphql/groups/has_aggregates.rb +2 -7
- data/lib/graphql/groups/has_groups.rb +19 -6
- data/lib/graphql/groups/pending_query.rb +22 -0
- data/lib/graphql/groups/query_builder.rb +96 -0
- data/lib/graphql/groups/query_builder_context.rb +21 -0
- data/lib/graphql/groups/query_result.rb +17 -0
- data/lib/graphql/groups/result_transformer.rb +31 -61
- data/lib/graphql/groups/schema/group_field.rb +0 -1
- data/lib/graphql/groups/schema/group_result_type.rb +1 -3
- data/lib/graphql/groups/schema/group_type.rb +1 -2
- data/lib/graphql/groups/utils.rb +49 -0
- data/lib/graphql/groups/version.rb +3 -1
- data/lib/graphql/groups.rb +8 -6
- metadata +25 -12
- data/.github/workflows/build.yml +0 -28
- data/benchmark/benchmark.jpg +0 -0
- data/benchmark/benchmark.rb +0 -101
- data/benchmark/benchmark_schema.rb +0 -53
- data/lib/graphql/groups/executor.rb +0 -40
- data/lib/graphql/groups/lookahead_parser.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 402086494f2bba5acc764ad745f9083b13b1f9986f43c9717fb511647c51aa11
|
4
|
+
data.tar.gz: 043ea5a04163d20f31b7ea6becac5da0dfda5b3d9721b97798acabb2bd8a9974
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90e97ff34a842f148d49fcaa6c39bd6605c32bbe9f19e7f7b1d9d2b6ac3562da4fdfb1deff38fe110c83b980229225c45ceff75b3f97d14e0f71791eba610862
|
7
|
+
data.tar.gz: 5b561e1e307a1a9618316f4bf695bc96273dd67e156f629ddf3a4cc5a9fdf3d60ada09fce0857b3fdf9575ce1b7425c08864163561d4c18f0912eb2a22a6ec56
|
data/Gemfile.lock
CHANGED
@@ -1,28 +1,28 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
graphql-groups (0.
|
4
|
+
graphql-groups (0.2.0)
|
5
5
|
graphql (~> 1, > 1.9)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
activemodel (6.
|
11
|
-
activesupport (= 6.
|
12
|
-
activerecord (6.
|
13
|
-
activemodel (= 6.
|
14
|
-
activesupport (= 6.
|
15
|
-
activesupport (6.
|
10
|
+
activemodel (6.1.3)
|
11
|
+
activesupport (= 6.1.3)
|
12
|
+
activerecord (6.1.3)
|
13
|
+
activemodel (= 6.1.3)
|
14
|
+
activesupport (= 6.1.3)
|
15
|
+
activesupport (6.1.3)
|
16
16
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
17
|
-
i18n (>=
|
18
|
-
minitest (
|
19
|
-
tzinfo (~>
|
20
|
-
zeitwerk (~> 2.
|
21
|
-
addressable (2.
|
17
|
+
i18n (>= 1.6, < 2)
|
18
|
+
minitest (>= 5.1)
|
19
|
+
tzinfo (~> 2.0)
|
20
|
+
zeitwerk (~> 2.3)
|
21
|
+
addressable (2.8.0)
|
22
22
|
public_suffix (>= 2.0.2, < 5.0)
|
23
23
|
ast (2.4.1)
|
24
24
|
benchmark-ips (2.8.2)
|
25
|
-
concurrent-ruby (1.1.
|
25
|
+
concurrent-ruby (1.1.8)
|
26
26
|
database_cleaner (1.8.5)
|
27
27
|
database_cleaner-active_record (1.8.0)
|
28
28
|
activerecord
|
@@ -36,6 +36,8 @@ GEM
|
|
36
36
|
http (> 0.8, < 3.0)
|
37
37
|
multi_json (~> 1)
|
38
38
|
graphql (1.11.1)
|
39
|
+
groupdate (5.2.1)
|
40
|
+
activesupport (>= 5)
|
39
41
|
gruff (0.10.0)
|
40
42
|
histogram
|
41
43
|
rmagick
|
@@ -50,18 +52,18 @@ GEM
|
|
50
52
|
domain_name (~> 0.5)
|
51
53
|
http-form_data (1.0.3)
|
52
54
|
http_parser.rb (0.6.0)
|
53
|
-
i18n (1.8.
|
55
|
+
i18n (1.8.9)
|
54
56
|
concurrent-ruby (~> 1.0)
|
55
|
-
minitest (5.14.
|
57
|
+
minitest (5.14.4)
|
56
58
|
multi_json (1.15.0)
|
57
59
|
parallel (1.19.2)
|
58
60
|
parser (2.7.1.4)
|
59
61
|
ast (~> 2.4.1)
|
60
|
-
public_suffix (4.0.
|
62
|
+
public_suffix (4.0.6)
|
61
63
|
rainbow (3.0.0)
|
62
64
|
rake (13.0.1)
|
63
65
|
regexp_parser (1.7.1)
|
64
|
-
rexml (3.2.
|
66
|
+
rexml (3.2.5)
|
65
67
|
rmagick (4.1.2)
|
66
68
|
rspec (3.9.0)
|
67
69
|
rspec-core (~> 3.9.0)
|
@@ -95,14 +97,13 @@ GEM
|
|
95
97
|
simplecov-html (~> 0.11)
|
96
98
|
simplecov-html (0.12.2)
|
97
99
|
sqlite3 (1.4.2)
|
98
|
-
|
99
|
-
|
100
|
-
thread_safe (~> 0.1)
|
100
|
+
tzinfo (2.0.4)
|
101
|
+
concurrent-ruby (~> 1.0)
|
101
102
|
unf (0.1.4)
|
102
103
|
unf_ext
|
103
104
|
unf_ext (0.0.7.7)
|
104
105
|
unicode-display_width (1.7.0)
|
105
|
-
zeitwerk (2.4.
|
106
|
+
zeitwerk (2.4.2)
|
106
107
|
|
107
108
|
PLATFORMS
|
108
109
|
ruby
|
@@ -114,6 +115,7 @@ DEPENDENCIES
|
|
114
115
|
database_cleaner-active_record (~> 1.8)
|
115
116
|
gqli (~> 1.0)
|
116
117
|
graphql-groups!
|
118
|
+
groupdate (~> 5.2.1)
|
117
119
|
gruff (~> 0.10)
|
118
120
|
rake (~> 13.0)
|
119
121
|
rspec (~> 3.0)
|
data/README.md
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
[![Maintainability](https://api.codeclimate.com/v1/badges/692d4125ac8548fb145e/maintainability)](https://codeclimate.com/github/hschne/graphql-groups/maintainability)
|
6
6
|
[![Test Coverage](https://api.codeclimate.com/v1/badges/692d4125ac8548fb145e/test_coverage)](https://codeclimate.com/github/hschne/graphql-groups/test_coverage)
|
7
7
|
|
8
|
-
|
8
|
+
Run group- and aggregation queries with [graphql-ruby](https://github.com/rmosolgo/graphql-ruby).
|
9
9
|
|
10
10
|
## Installation
|
11
11
|
|
@@ -20,13 +20,12 @@ $ bundle install
|
|
20
20
|
|
21
21
|
## Usage
|
22
22
|
|
23
|
-
|
23
|
+
Suppose you want to get the number of authors, grouped by their age. Create a new group type by inheriting from `GraphQL::Groups::GroupType`:
|
24
24
|
|
25
25
|
```ruby
|
26
|
-
class AuthorGroupType < GraphQL::Groups::GroupType
|
26
|
+
class AuthorGroupType < GraphQL::Groups::Schema::GroupType
|
27
27
|
scope { Author.all }
|
28
28
|
|
29
|
-
by :name
|
30
29
|
by :age
|
31
30
|
end
|
32
31
|
```
|
@@ -37,15 +36,14 @@ Include the new type in your schema using the `group` keyword, and you are done.
|
|
37
36
|
class QueryType < GraphQL::Schema::Object
|
38
37
|
include GraphQL::Groups
|
39
38
|
|
40
|
-
group :
|
39
|
+
group :author_group_by, AuthorGroupType
|
41
40
|
end
|
42
41
|
```
|
43
42
|
|
44
|
-
You can then run
|
45
|
-
|
43
|
+
You can then run the following query to retrieve the number of authors per age.
|
46
44
|
```graphql
|
47
45
|
query myQuery{
|
48
|
-
|
46
|
+
authorGroupBy {
|
49
47
|
age {
|
50
48
|
key
|
51
49
|
count
|
@@ -55,7 +53,7 @@ query myQuery{
|
|
55
53
|
```
|
56
54
|
```json
|
57
55
|
{
|
58
|
-
"
|
56
|
+
"authorGroupBy":{
|
59
57
|
"age":[
|
60
58
|
{
|
61
59
|
"key":"31",
|
@@ -71,18 +69,21 @@ query myQuery{
|
|
71
69
|
}
|
72
70
|
```
|
73
71
|
|
72
|
+
|
74
73
|
## Why?
|
75
74
|
|
76
|
-
`graphql-ruby` lacks a built in way to
|
77
|
-
|
78
|
-
|
75
|
+
`graphql-ruby` lacks a built in way to retrieve statistical data, such as counts or averages. It is possible to implement custom queries that provide this functionality by using `group_by` (see for example [here](https://dev.to/gopeter/how-to-add-a-groupby-field-to-your-graphql-api-1f2j)), but this performs poorly for large amounts of data.
|
76
|
+
|
77
|
+
`graphql-groups` allows you to write flexible, readable queries while leveraging your database to aggreate data. It does so by performing an AST analysis on your request and executing exactly the database queries needed to fulfill it. This performs much better than grouping and aggregating in memory. See [performance](#Performance) for a benchmark.
|
79
78
|
|
80
|
-
`graphql-groups` allows you to write flexible, readable queries while leveraging your database to group and
|
81
|
-
aggregate data. See [performance](#Performance) for a benchmark.
|
82
79
|
|
83
80
|
## Advanced Usage
|
84
81
|
|
85
|
-
|
82
|
+
For a showcase of what you can do with `graphql-groups` check out [graphql-groups-demo](https://github.com/hschne/graphql-groups-demo)
|
83
|
+
|
84
|
+
Find a hosted version of the demo app [on Heroku](https://graphql-groups-demo.herokuapp.com/).
|
85
|
+
|
86
|
+
### Grouping by Multiple Attributes
|
86
87
|
|
87
88
|
This library really shines when you want to group by multiple attributes, or otherwise retrieve complex statistical information
|
88
89
|
within a single GraphQL query.
|
@@ -134,7 +135,7 @@ query myQuery{
|
|
134
135
|
|
135
136
|
`graphql-groups` will automatically execute the required queries and return the results in a easily parsable response.
|
136
137
|
|
137
|
-
|
138
|
+
### Custom Grouping Queries
|
138
139
|
|
139
140
|
To customize which queries are executed to group items, you may specify the grouping query by creating a method of the same name in the group type.
|
140
141
|
|
@@ -150,7 +151,7 @@ class AuthorGroupType < GraphQL::Groups::Schema::GroupType
|
|
150
151
|
end
|
151
152
|
```
|
152
153
|
|
153
|
-
You may also pass arguments to custom grouping queries. In this case, pass any arguments to your group query as keyword arguments.
|
154
|
+
You may also pass arguments to custom grouping queries. In this case, pass any arguments to your group query as keyword arguments.
|
154
155
|
|
155
156
|
```ruby
|
156
157
|
class BookGroupType < GraphQL::Groups::Schema::GroupType
|
@@ -173,6 +174,23 @@ class BookGroupType < GraphQL::Groups::Schema::GroupType
|
|
173
174
|
end
|
174
175
|
```
|
175
176
|
|
177
|
+
You may access the query `context` in custom queries. As opposed to resolver methods accessing `object` is not possible and will raise an error.
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class BookGroupType < GraphQL::Groups::Schema::GroupType
|
181
|
+
scope { Book.all }
|
182
|
+
|
183
|
+
by :list_price
|
184
|
+
|
185
|
+
def list_price(scope:)
|
186
|
+
currency = context[:currency] || ' $'
|
187
|
+
scope.group("list_price || ' #{currency}'")
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
### Custom Scopes
|
193
|
+
|
176
194
|
When defining a group type's scope you may access the parents `object` and `context`.
|
177
195
|
|
178
196
|
```ruby
|
@@ -198,8 +216,6 @@ class BookGroupType < GraphQL::Groups::Schema::GroupType
|
|
198
216
|
end
|
199
217
|
```
|
200
218
|
|
201
|
-
For more examples see the [feature spec](./spec/graphql/feature_spec.rb) and [test schema](./spec/graphql/support/test_schema.rb)
|
202
|
-
|
203
219
|
### Custom Aggregates
|
204
220
|
|
205
221
|
Per default `graphql-groups` supports aggregating `count` out of the box. If you need to other aggregates, such as sum or average
|
@@ -253,18 +269,17 @@ While it is possible to add grouping to your GraphQL schema by using `group_by`
|
|
253
269
|
|
254
270
|
The benchmark queries the author count grouped by name, using an increasing number of authors. While the in-memory approach of grouping works well for a small number of records, it is outperformed quickly as that number increases.
|
255
271
|
|
256
|
-
Benchmarks
|
272
|
+
Benchmarks can be generated by running `rake benchmark`. The benchmark script used to generate the report be found [here](./benchmark/benchmark.rb)
|
257
273
|
|
258
274
|
## Limitations and Known Issues
|
259
275
|
|
260
|
-
|
261
|
-
that this libraries API will not change fundamentally from one release to the next. Please refer to the [issue tracker](https://github.com/hschne/graphql-groups/issues) for a list of known issues.
|
276
|
+
Please refer to the [issue tracker](https://github.com/hschne/graphql-groups/issues) for a list of known issues.
|
262
277
|
|
263
278
|
## Credits
|
264
279
|
|
265
|
-
|
280
|
+
<a href="https://www.meisterlabs.com"><img src="Meister.png" width="50%"></a>
|
266
281
|
|
267
|
-
graphql-groups
|
282
|
+
[graphql-groups](https://github.com/hschne/graphql-groups) was created at [meister](https://www.meisterlabs.com/)
|
268
283
|
|
269
284
|
## Development
|
270
285
|
|
data/graphql-groups.gemspec
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
lib = File.expand_path('lib', __dir__)
|
2
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
5
|
require 'graphql/groups/version'
|
@@ -8,10 +10,10 @@ Gem::Specification.new do |spec|
|
|
8
10
|
spec.authors = ['Hans-Jörg Schnedlitz']
|
9
11
|
spec.email = ['hans.schnedlitz@gmail.com']
|
10
12
|
|
11
|
-
spec.summary = 'Create flexible and
|
13
|
+
spec.summary = 'Create flexible and fast aggregation queries with graphql-ruby'
|
12
14
|
spec.description = <<~HEREDOC
|
13
15
|
GraphQL Groups makes it easy to add aggregation queries to your GraphQL schema. It combines a simple, flexible
|
14
|
-
schema definition with high performance
|
16
|
+
schema definition with high performance
|
15
17
|
HEREDOC
|
16
18
|
spec.homepage = 'https://github.com/hschne/graphql-groups'
|
17
19
|
spec.license = 'MIT'
|
@@ -22,18 +24,21 @@ Gem::Specification.new do |spec|
|
|
22
24
|
|
23
25
|
# Specify which files should be added to the gem when it is released.
|
24
26
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
-
spec.files = Dir.chdir(File.expand_path(
|
26
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
27
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
28
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|benchmark|.github)/}) }
|
27
29
|
end
|
28
30
|
spec.bindir = 'exe'
|
29
31
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
32
|
spec.require_paths = ['lib']
|
31
33
|
|
34
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
|
35
|
+
|
32
36
|
spec.add_development_dependency 'activerecord', '~> 6.0'
|
33
37
|
spec.add_development_dependency 'benchmark-ips', '~> 2.8'
|
34
38
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
35
39
|
spec.add_development_dependency 'database_cleaner-active_record', '~> 1.8'
|
36
40
|
spec.add_development_dependency 'gqli', '~> 1.0'
|
41
|
+
spec.add_development_dependency 'groupdate', '~> 5.2.1'
|
37
42
|
spec.add_development_dependency 'gruff', '~> 0.10'
|
38
43
|
spec.add_development_dependency 'rake', '~> 13.0'
|
39
44
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
@@ -11,7 +11,6 @@ module GraphQL
|
|
11
11
|
def aggregate(name, *_, **options, &block)
|
12
12
|
aggregate_type = aggregate_type(name)
|
13
13
|
|
14
|
-
# TODO: Handle method name conflicts, or no query method found error
|
15
14
|
resolve_method = "resolve_#{name}".to_sym
|
16
15
|
query_method = options[:query_method] || name
|
17
16
|
field = aggregate_field name, aggregate_type,
|
@@ -21,11 +20,8 @@ module GraphQL
|
|
21
20
|
**options, &block
|
22
21
|
aggregate_type.add_fields(field.own_attributes)
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
scope = kwargs[:scope]
|
27
|
-
attribute = kwargs[:attribute]
|
28
|
-
scope.public_send(name, attribute)
|
23
|
+
define_method query_method do |scope:, **kwargs|
|
24
|
+
scope.public_send(name, **kwargs)
|
29
25
|
end
|
30
26
|
|
31
27
|
define_method resolve_method do
|
@@ -42,7 +38,6 @@ module GraphQL
|
|
42
38
|
private
|
43
39
|
|
44
40
|
def aggregate_type(name)
|
45
|
-
# TODO: Handle no aggregate type found
|
46
41
|
name = "#{name}AggregateType".upcase_first
|
47
42
|
own_aggregate_types[name] ||= Class.new(Schema::AggregateType) do
|
48
43
|
graphql_name name
|
@@ -17,28 +17,27 @@ module GraphQL
|
|
17
17
|
module ClassMethods
|
18
18
|
attr_reader :class_scope
|
19
19
|
|
20
|
-
# TODO: Error if there are no groupings defined
|
21
20
|
def by(name, **options, &block)
|
22
21
|
query_method = options[:query_method] || name
|
23
22
|
resolver_method = "resolve_#{query_method}".to_sym
|
24
23
|
group_field name, [own_result_type],
|
25
|
-
null: false,
|
26
|
-
resolver_method: resolver_method,
|
27
24
|
query_method: query_method,
|
25
|
+
null: true,
|
26
|
+
resolver_method: resolver_method,
|
27
|
+
|
28
28
|
**options, &block
|
29
29
|
|
30
30
|
define_method query_method do |**kwargs|
|
31
31
|
kwargs[:scope].group(name)
|
32
32
|
end
|
33
33
|
|
34
|
-
# TODO: Warn / Disallow overwriting these resolver methods
|
35
34
|
define_method resolver_method do |**_|
|
36
35
|
group[name]
|
37
36
|
end
|
38
37
|
end
|
39
38
|
|
40
39
|
def group_field(*args, **kwargs, &block)
|
41
|
-
field_defn =
|
40
|
+
field_defn = field_class.from_options(*args, owner: self, **kwargs, &block)
|
42
41
|
add_field(field_defn)
|
43
42
|
field_defn
|
44
43
|
end
|
@@ -65,7 +64,21 @@ module GraphQL
|
|
65
64
|
field :group_by, own_group_type, null: false, camelize: true
|
66
65
|
|
67
66
|
def group_by
|
68
|
-
group_result[1][:
|
67
|
+
group_result[1][:group_by]
|
68
|
+
end
|
69
|
+
end)
|
70
|
+
end
|
71
|
+
|
72
|
+
def own_field_type
|
73
|
+
type = "#{name}Field"
|
74
|
+
base_field_type = field_class
|
75
|
+
registry = GraphQL::Groups::GroupTypeRegistry.instance
|
76
|
+
registry.get(type) || registry.register(type, Class.new(base_field_type) do
|
77
|
+
attr_reader :query_method
|
78
|
+
|
79
|
+
def initialize(query_method:, **options, &definition_block)
|
80
|
+
@query_method = query_method
|
81
|
+
super(**options.except(:query_method), &definition_block)
|
69
82
|
end
|
70
83
|
end)
|
71
84
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
class PendingQuery
|
6
|
+
attr_reader :key
|
7
|
+
attr_reader :aggregate
|
8
|
+
attr_reader :query
|
9
|
+
|
10
|
+
def initialize(key, aggregate, proc)
|
11
|
+
@key = Utils.wrap(key)
|
12
|
+
@aggregate = Utils.wrap(aggregate)
|
13
|
+
@query = proc
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute
|
17
|
+
result = @query.call
|
18
|
+
QueryResult.new(@key, @aggregate, result)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
class QueryBuilder
|
6
|
+
def self.parse(lookahead, object, context)
|
7
|
+
QueryBuilder.new(lookahead, object, context).group_selections
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(lookahead, object, context)
|
11
|
+
@lookahead = lookahead
|
12
|
+
@context = context
|
13
|
+
type = @lookahead.field.type.of_type
|
14
|
+
@base_query = proc { type.authorized_new(object, context).scope }
|
15
|
+
super()
|
16
|
+
end
|
17
|
+
|
18
|
+
def group_selections(lookahead = @lookahead, current_context = QueryBuilderContext.new([], @base_query))
|
19
|
+
selections = lookahead.selections
|
20
|
+
group_field_type = lookahead.field.type.of_type.field_class
|
21
|
+
group_selections = selections.select { |selection| selection.field.is_a?(group_field_type) }
|
22
|
+
queries = group_selections.each_with_object([]) do |selection, object|
|
23
|
+
field_proc = proc_from_selection(selection.field, selection.arguments)
|
24
|
+
context = current_context.update(selection.name, field_proc)
|
25
|
+
object << create_pending_queries(selection, context)
|
26
|
+
end
|
27
|
+
nested_queries = group_selections
|
28
|
+
.filter { |selection| selection.selects?(:group_by) }
|
29
|
+
.each_with_object([]) do |selection, object|
|
30
|
+
field_proc = proc_from_selection(selection.field, selection.arguments)
|
31
|
+
context = current_context.update(selection.name, field_proc)
|
32
|
+
object << group_selections(selection.selection(:group_by), context)
|
33
|
+
end
|
34
|
+
(queries + nested_queries).flatten
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def create_pending_queries(current_selection, context)
|
40
|
+
aggregate_selections = current_selection
|
41
|
+
.selections
|
42
|
+
.select { |selection| selection.field.is_a?(GraphQL::Groups::Schema::AggregateField) }
|
43
|
+
count_queries = count_queries(aggregate_selections, context)
|
44
|
+
aggregate_queries = aggregate_queries(aggregate_selections, context)
|
45
|
+
(count_queries + aggregate_queries)
|
46
|
+
end
|
47
|
+
|
48
|
+
def count_queries(aggregate_selections, context)
|
49
|
+
# TODO: When getting multiple aggregates for the same base data we could do just a single query instead of many
|
50
|
+
aggregate_selections
|
51
|
+
.select { |selection| selection.name == :count }
|
52
|
+
.map do |selection|
|
53
|
+
field = selection.field
|
54
|
+
count_proc = proc { |scope| field.owner.send(:new, {}, nil).public_send(field.query_method, scope: scope) }
|
55
|
+
combined = combine_procs(context.current_proc, count_proc)
|
56
|
+
PendingQuery.new(context.grouping, selection.name, combined)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def aggregate_queries(aggregate_selections, context)
|
61
|
+
aggregate_selections
|
62
|
+
.select { |selection| selection.field.own_attributes.present? }
|
63
|
+
.map { |selection| attribute_queries(context, selection) }
|
64
|
+
.flatten
|
65
|
+
end
|
66
|
+
|
67
|
+
def attribute_queries(context, selection)
|
68
|
+
selection.field
|
69
|
+
.own_attributes
|
70
|
+
.select { |attribute| selection.selections.map(&:name).include?(attribute) }
|
71
|
+
.map do |attribute|
|
72
|
+
aggregate_proc = proc_from_attribute(selection.field, attribute, selection.arguments)
|
73
|
+
combined = combine_procs(context.current_proc, aggregate_proc)
|
74
|
+
PendingQuery.new(context.grouping, [selection.name, attribute], combined)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def combine_procs(base_proc, new_proc)
|
79
|
+
proc { new_proc.call(base_proc.call) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def proc_from_selection(field, arguments)
|
83
|
+
proc { |scope| field.owner.authorized_new(nil, @context).public_send(field.query_method, scope: scope, **arguments) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def proc_from_attribute(field, attribute, arguments)
|
87
|
+
proc do |scope|
|
88
|
+
field.owner.authorized_new(nil, @context)
|
89
|
+
.public_send(field.query_method,
|
90
|
+
scope: scope,
|
91
|
+
attribute: attribute, **arguments)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class QueryBuilderContext
|
4
|
+
attr_reader :grouping
|
5
|
+
attr_reader :current_proc
|
6
|
+
|
7
|
+
def initialize(groupings = [], current_proc = nil)
|
8
|
+
@grouping = groupings
|
9
|
+
@current_proc = current_proc
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(grouping, new_proc)
|
13
|
+
new_grouping = @grouping + [grouping]
|
14
|
+
combined_proc = combine_procs(@current_proc, new_proc)
|
15
|
+
QueryBuilderContext.new(new_grouping, combined_proc)
|
16
|
+
end
|
17
|
+
|
18
|
+
def combine_procs(base_proc, new_proc)
|
19
|
+
proc { new_proc.call(base_proc.call) }
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
class QueryResult
|
6
|
+
attr_reader :key
|
7
|
+
attr_reader :aggregate
|
8
|
+
attr_reader :result_hash
|
9
|
+
|
10
|
+
def initialize(key, aggregate, result)
|
11
|
+
@key = Utils.wrap(key)
|
12
|
+
@aggregate = Utils.wrap(aggregate)
|
13
|
+
@result_hash = result
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -3,77 +3,47 @@
|
|
3
3
|
module GraphQL
|
4
4
|
module Groups
|
5
5
|
class ResultTransformer
|
6
|
-
def run(
|
7
|
-
|
6
|
+
def run(query_results)
|
7
|
+
# Sort by key length so that deeper nested queries come later
|
8
|
+
query_results
|
9
|
+
.sort_by { |query_result| query_result.key.length }
|
10
|
+
.each_with_object({}) do |query_result, object|
|
11
|
+
transform_result(query_result, object)
|
12
|
+
end
|
8
13
|
end
|
9
14
|
|
10
15
|
private
|
11
16
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
results.each_with_object({}) { |(key, value), object| object.deep_merge!(transform_result(key, value)) }
|
24
|
-
end
|
25
|
-
|
26
|
-
def transform_result(key, result)
|
27
|
-
transformed = result.each_with_object({}) do |(aggregate_key, aggregate_value), object|
|
28
|
-
if aggregate_value.values.any? { |x| x.is_a?(Hash) }
|
29
|
-
aggregate_value.each do |attribute, value|
|
30
|
-
object.deep_merge!(transform_attribute(key, aggregate_key, attribute, value))
|
31
|
-
end
|
17
|
+
def transform_result(query_result, object)
|
18
|
+
keys = query_result.key
|
19
|
+
return object[keys[0]] = [] if query_result.result_hash.empty?
|
20
|
+
|
21
|
+
query_result.result_hash.each do |grouping_result|
|
22
|
+
group_result_keys = Utils.wrap(grouping_result[0])
|
23
|
+
group_result_value = grouping_result[1]
|
24
|
+
Utils.duplicate(keys, group_result_keys)
|
25
|
+
inner_hash = create_nested_result(keys, group_result_keys, object)
|
26
|
+
if query_result.aggregate.length == 1
|
27
|
+
inner_hash[query_result.aggregate[0]] = group_result_value
|
32
28
|
else
|
33
|
-
|
29
|
+
aggregate_type = query_result.aggregate[0]
|
30
|
+
aggregate_attribute = query_result.aggregate[1]
|
31
|
+
inner_hash[aggregate_type] ||= {}
|
32
|
+
inner_hash[aggregate_type][aggregate_attribute] ||= group_result_value
|
34
33
|
end
|
35
34
|
end
|
36
|
-
|
37
|
-
transformed.presence || { key => [] }
|
38
35
|
end
|
39
36
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
end
|
37
|
+
def create_nested_result(keys, group_result_keys, object)
|
38
|
+
head_key, *rest_keys = keys
|
39
|
+
head_group_key, *rest_group_keys = group_result_keys
|
40
|
+
object[head_key] ||= {}
|
41
|
+
object[head_key][head_group_key] ||= {}
|
42
|
+
inner_hash = object[head_key][head_group_key]
|
43
|
+
return inner_hash if rest_keys.empty?
|
49
44
|
|
50
|
-
|
51
|
-
|
52
|
-
with_zipped = build_keys(key, keys)
|
53
|
-
with_zipped.append(aggregate)
|
54
|
-
with_zipped.append(attribute)
|
55
|
-
hash = with_zipped.reverse.inject(value) { |a, n| { n => a } }
|
56
|
-
object.deep_merge!(hash)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def build_keys(key, keys)
|
61
|
-
key = wrap(key)
|
62
|
-
keys = keys ? wrap(keys) : [nil]
|
63
|
-
nested = [:nested] * (key.length - 1)
|
64
|
-
|
65
|
-
with_zipped = key.zip(keys).zip(nested).flatten!
|
66
|
-
with_zipped.first(with_zipped.size - 1)
|
67
|
-
end
|
68
|
-
|
69
|
-
def wrap(object)
|
70
|
-
if object.nil?
|
71
|
-
[]
|
72
|
-
elsif object.respond_to?(:to_ary)
|
73
|
-
object.to_ary || [object]
|
74
|
-
else
|
75
|
-
[object]
|
76
|
-
end
|
45
|
+
inner_hash[:group_by] ||= {}
|
46
|
+
create_nested_result(rest_keys, rest_group_keys, inner_hash[:group_by])
|
77
47
|
end
|
78
48
|
end
|
79
49
|
end
|
@@ -13,15 +13,13 @@ module GraphQL
|
|
13
13
|
|
14
14
|
field :key, String, null: true
|
15
15
|
|
16
|
-
field :count, Integer, null: false
|
17
|
-
|
18
16
|
aggregate_field :count, Integer, null: false, query_method: :count, resolver_method: :resolve_count
|
19
17
|
|
20
18
|
def key
|
21
19
|
group_result[0]
|
22
20
|
end
|
23
21
|
|
24
|
-
def count(scope:, **
|
22
|
+
def count(scope:, **)
|
25
23
|
scope.size
|
26
24
|
end
|
27
25
|
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
module Utils
|
6
|
+
class << self
|
7
|
+
def wrap(object)
|
8
|
+
if object.nil?
|
9
|
+
[]
|
10
|
+
elsif object.respond_to?(:to_ary)
|
11
|
+
object.to_ary || [object]
|
12
|
+
else
|
13
|
+
[object]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# This is used by the resul transformer when the user executed a query where some groupings are repeated, so depth
|
18
|
+
# of the query doesn't match the length of the query result keys. We need to modify the result keys so everything
|
19
|
+
# matches again.
|
20
|
+
def duplicate(keys, values)
|
21
|
+
return if keys.length == values.length
|
22
|
+
|
23
|
+
duplicates = duplicates(keys)
|
24
|
+
return if duplicates.empty?
|
25
|
+
|
26
|
+
duplicates.each do |_, indices|
|
27
|
+
first_occurrence, *rest = indices
|
28
|
+
value_to_duplicate = values[first_occurrence]
|
29
|
+
rest.each { |index| values.insert(index, value_to_duplicate) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def duplicates(array)
|
36
|
+
map = {}
|
37
|
+
duplicates = {}
|
38
|
+
array.each_with_index do |v, i|
|
39
|
+
map[v] = (map[v] || 0) + 1
|
40
|
+
duplicates[v] ||= []
|
41
|
+
duplicates[v] << i
|
42
|
+
end
|
43
|
+
duplicates.select { |_, v| v.length > 1 }
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/graphql/groups.rb
CHANGED
@@ -4,6 +4,7 @@ require 'graphql/groups/version'
|
|
4
4
|
|
5
5
|
require 'graphql'
|
6
6
|
|
7
|
+
require 'graphql/groups/utils'
|
7
8
|
require 'graphql/groups/group_type_registry'
|
8
9
|
require 'graphql/groups/schema/group_field'
|
9
10
|
require 'graphql/groups/schema/aggregate_field'
|
@@ -15,9 +16,11 @@ require 'graphql/groups/has_groups'
|
|
15
16
|
require 'graphql/groups/schema/group_result_type'
|
16
17
|
require 'graphql/groups/schema/group_type'
|
17
18
|
|
18
|
-
require 'graphql/groups/
|
19
|
+
require 'graphql/groups/query_result'
|
20
|
+
require 'graphql/groups/pending_query'
|
21
|
+
require 'graphql/groups/query_builder_context'
|
22
|
+
require 'graphql/groups/query_builder'
|
19
23
|
require 'graphql/groups/result_transformer'
|
20
|
-
require 'graphql/groups/executor'
|
21
24
|
|
22
25
|
|
23
26
|
module GraphQL
|
@@ -31,10 +34,9 @@ module GraphQL
|
|
31
34
|
field name, type, extras: [:lookahead], null: false, **options
|
32
35
|
|
33
36
|
define_method name do |lookahead: nil|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
GraphQL::Groups::ResultTransformer.new.run(results)
|
37
|
+
pending_queries = QueryBuilder.parse(lookahead, object, context)
|
38
|
+
query_results = pending_queries.map(&:execute)
|
39
|
+
GraphQL::Groups::ResultTransformer.new.run(query_results)
|
38
40
|
end
|
39
41
|
end
|
40
42
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-groups
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Hans-Jörg Schnedlitz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-11-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '1.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: groupdate
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 5.2.1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 5.2.1
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: gruff
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -199,14 +213,13 @@ dependencies:
|
|
199
213
|
- !ruby/object:Gem::Version
|
200
214
|
version: '1.9'
|
201
215
|
description: "GraphQL Groups makes it easy to add aggregation queries to your GraphQL
|
202
|
-
schema. It combines a simple, flexible \nschema definition with high performance
|
216
|
+
schema. It combines a simple, flexible \nschema definition with high performance\n"
|
203
217
|
email:
|
204
218
|
- hans.schnedlitz@gmail.com
|
205
219
|
executables: []
|
206
220
|
extensions: []
|
207
221
|
extra_rdoc_files: []
|
208
222
|
files:
|
209
|
-
- ".github/workflows/build.yml"
|
210
223
|
- ".gitignore"
|
211
224
|
- ".rspec"
|
212
225
|
- ".rubocop.yml"
|
@@ -219,24 +232,24 @@ files:
|
|
219
232
|
- Meister.png
|
220
233
|
- README.md
|
221
234
|
- Rakefile
|
222
|
-
- benchmark/benchmark.jpg
|
223
|
-
- benchmark/benchmark.rb
|
224
|
-
- benchmark/benchmark_schema.rb
|
225
235
|
- bin/console
|
226
236
|
- bin/setup
|
227
237
|
- graphql-groups.gemspec
|
228
238
|
- lib/graphql/groups.rb
|
229
|
-
- lib/graphql/groups/executor.rb
|
230
239
|
- lib/graphql/groups/group_type_registry.rb
|
231
240
|
- lib/graphql/groups/has_aggregates.rb
|
232
241
|
- lib/graphql/groups/has_groups.rb
|
233
|
-
- lib/graphql/groups/
|
242
|
+
- lib/graphql/groups/pending_query.rb
|
243
|
+
- lib/graphql/groups/query_builder.rb
|
244
|
+
- lib/graphql/groups/query_builder_context.rb
|
245
|
+
- lib/graphql/groups/query_result.rb
|
234
246
|
- lib/graphql/groups/result_transformer.rb
|
235
247
|
- lib/graphql/groups/schema/aggregate_field.rb
|
236
248
|
- lib/graphql/groups/schema/aggregate_type.rb
|
237
249
|
- lib/graphql/groups/schema/group_field.rb
|
238
250
|
- lib/graphql/groups/schema/group_result_type.rb
|
239
251
|
- lib/graphql/groups/schema/group_type.rb
|
252
|
+
- lib/graphql/groups/utils.rb
|
240
253
|
- lib/graphql/groups/version.rb
|
241
254
|
homepage: https://github.com/hschne/graphql-groups
|
242
255
|
licenses:
|
@@ -253,15 +266,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
253
266
|
requirements:
|
254
267
|
- - ">="
|
255
268
|
- !ruby/object:Gem::Version
|
256
|
-
version:
|
269
|
+
version: 2.6.0
|
257
270
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
258
271
|
requirements:
|
259
272
|
- - ">="
|
260
273
|
- !ruby/object:Gem::Version
|
261
274
|
version: '0'
|
262
275
|
requirements: []
|
263
|
-
rubygems_version: 3.0.3
|
276
|
+
rubygems_version: 3.0.3.1
|
264
277
|
signing_key:
|
265
278
|
specification_version: 4
|
266
|
-
summary: Create flexible and
|
279
|
+
summary: Create flexible and fast aggregation queries with graphql-ruby
|
267
280
|
test_files: []
|
data/.github/workflows/build.yml
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
name: Build
|
2
|
-
|
3
|
-
on:
|
4
|
-
push:
|
5
|
-
branches: [ master ]
|
6
|
-
pull_request:
|
7
|
-
branches: [ master ]
|
8
|
-
|
9
|
-
jobs:
|
10
|
-
test:
|
11
|
-
runs-on: ubuntu-latest
|
12
|
-
steps:
|
13
|
-
- uses: actions/checkout@v2
|
14
|
-
- name: Set up Ruby
|
15
|
-
uses: ruby/setup-ruby@v1
|
16
|
-
with:
|
17
|
-
ruby-version: 2.6.5
|
18
|
-
- name: Install dependencies
|
19
|
-
run: bundle install
|
20
|
-
- name: Run tests
|
21
|
-
run: bundle exec rspec
|
22
|
-
- name: Report coverage
|
23
|
-
uses: paambaati/codeclimate-action@v2.6.0
|
24
|
-
env:
|
25
|
-
CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}}
|
26
|
-
with:
|
27
|
-
# Coverage is already generated in test step, need to run anything again
|
28
|
-
coverageCommand: echo '' > /dev/null
|
data/benchmark/benchmark.jpg
DELETED
Binary file
|
data/benchmark/benchmark.rb
DELETED
@@ -1,101 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'graphql/groups'
|
4
|
-
|
5
|
-
require 'database_cleaner/active_record'
|
6
|
-
require 'gqli/dsl'
|
7
|
-
require 'benchmark/ips'
|
8
|
-
require 'gruff'
|
9
|
-
|
10
|
-
require_relative '../spec/graphql/support/test_schema/db'
|
11
|
-
require_relative '../spec/graphql/support/test_schema/models'
|
12
|
-
require_relative 'benchmark_schema'
|
13
|
-
|
14
|
-
class Compare
|
15
|
-
def run
|
16
|
-
puts 'Generating reports...'
|
17
|
-
datasets = perform_runs
|
18
|
-
labels = { 0 => '10', 1 => '100', 2 => '1000', 3 => '10000' }
|
19
|
-
puts 'Generating graph...'
|
20
|
-
graph = graph(labels, datasets)
|
21
|
-
graph.write('benchmark/benchmark.jpg')
|
22
|
-
puts 'Done!'
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def perform_runs
|
28
|
-
runs = [10, 100, 1000, 10_000]
|
29
|
-
runs.map { |count| single_run(count) }
|
30
|
-
.map(&:data)
|
31
|
-
.map { |data| { groups: data.first[:ips], group_by: data.second[:ips] } }
|
32
|
-
.each_with_object({ groups: [], group_by: [] }) do |item, object|
|
33
|
-
object[:groups].push(item[:groups])
|
34
|
-
object[:group_by].push(item[:group_by])
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def graph(labels, datasets)
|
39
|
-
g = Gruff::Bar.new(800)
|
40
|
-
g.title = 'graphql-groups vs group_by'
|
41
|
-
g.theme = Gruff::Themes::THIRTYSEVEN_SIGNALS
|
42
|
-
g.labels = labels
|
43
|
-
g.x_axis_label = 'records'
|
44
|
-
g.y_axis_label = 'iterations/second'
|
45
|
-
datasets.each do |data|
|
46
|
-
g.data(data[0], data[1])
|
47
|
-
end
|
48
|
-
g
|
49
|
-
end
|
50
|
-
|
51
|
-
def single_run(count)
|
52
|
-
DatabaseCleaner.strategy = :transaction
|
53
|
-
DatabaseCleaner.start
|
54
|
-
seed(count)
|
55
|
-
report = run_benchmark
|
56
|
-
DatabaseCleaner.clean
|
57
|
-
report
|
58
|
-
end
|
59
|
-
|
60
|
-
def seed(count)
|
61
|
-
names = %w[Ada Alice Bob Bruce]
|
62
|
-
count.times { Author.create(name: names.sample) }
|
63
|
-
end
|
64
|
-
|
65
|
-
def groups_query
|
66
|
-
GQLi::DSL.query {
|
67
|
-
fastGroups {
|
68
|
-
name {
|
69
|
-
key
|
70
|
-
count
|
71
|
-
}
|
72
|
-
}
|
73
|
-
}.to_gql
|
74
|
-
end
|
75
|
-
|
76
|
-
def naive_query
|
77
|
-
GQLi::DSL.query {
|
78
|
-
slowGroups {
|
79
|
-
name {
|
80
|
-
key
|
81
|
-
count
|
82
|
-
}
|
83
|
-
}
|
84
|
-
}.to_gql
|
85
|
-
end
|
86
|
-
|
87
|
-
def run_benchmark
|
88
|
-
Benchmark.ips(quiet: true) do |x|
|
89
|
-
# Configure the number of seconds used during
|
90
|
-
# the warmup phase (default 2) and calculation phase (default 5)
|
91
|
-
x.config(time: 2, warmup: 1)
|
92
|
-
|
93
|
-
x.report('groups') { PerformanceSchema.execute(groups_query) }
|
94
|
-
|
95
|
-
x.report('group-by') { PerformanceSchema.execute(naive_query) }
|
96
|
-
|
97
|
-
# Compare the iterations per second of the various reports!
|
98
|
-
x.compare!
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'graphql'
|
4
|
-
require 'graphql/groups'
|
5
|
-
|
6
|
-
class BaseType < GraphQL::Schema::Object; end
|
7
|
-
|
8
|
-
class AuthorGroupType < GraphQL::Groups::Schema::GroupType
|
9
|
-
scope { Author.all }
|
10
|
-
|
11
|
-
by :name
|
12
|
-
end
|
13
|
-
|
14
|
-
class SlowAuthorGroupResultType < GraphQL::Schema::Object
|
15
|
-
field :key, String, null: false
|
16
|
-
field :count, Integer, null: false
|
17
|
-
|
18
|
-
def key
|
19
|
-
object[0]
|
20
|
-
end
|
21
|
-
|
22
|
-
def count
|
23
|
-
object[1].size
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
class SlowAuthorGroupType < GraphQL::Schema::Object
|
28
|
-
field :name, [SlowAuthorGroupResultType], null: false
|
29
|
-
|
30
|
-
def name
|
31
|
-
object.group_by(&:name)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
class QueryType < BaseType
|
36
|
-
include GraphQL::Groups
|
37
|
-
|
38
|
-
group :fast_groups, AuthorGroupType, camelize: true
|
39
|
-
|
40
|
-
field :slow_groups, SlowAuthorGroupType, null: false, camelize: true
|
41
|
-
|
42
|
-
def slow_groups
|
43
|
-
Author.all
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
class PerformanceSchema < GraphQL::Schema
|
48
|
-
query QueryType
|
49
|
-
|
50
|
-
def self.resolve_type(_type, obj, _ctx)
|
51
|
-
"#{obj.class.name}Type"
|
52
|
-
end
|
53
|
-
end
|
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQL
|
4
|
-
module Groups
|
5
|
-
class Executor
|
6
|
-
class << self
|
7
|
-
def call(base_query, execution_plan)
|
8
|
-
execution_plan.each_with_object({}) do |(key, value), object|
|
9
|
-
object.merge!(execute(base_query, key, value))
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
def execute(scope, key, value)
|
14
|
-
group_query = value[:proc].call(scope: scope)
|
15
|
-
results = value[:aggregates].each_with_object({}) do |(aggregate_key, aggregate), object|
|
16
|
-
if aggregate_key == :count
|
17
|
-
object[:count] = aggregate[:proc].call(scope: group_query)
|
18
|
-
else
|
19
|
-
object[aggregate_key] ||= {}
|
20
|
-
aggregate[:attributes].each do |attribute|
|
21
|
-
result = aggregate[:proc].call(scope: group_query, attribute: attribute)
|
22
|
-
object[aggregate_key][attribute] = result
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
results = { key => results }
|
28
|
-
return results unless value[:nested]
|
29
|
-
|
30
|
-
value[:nested].each do |inner_key, inner_value|
|
31
|
-
new_key = (Array.wrap(key) << inner_key)
|
32
|
-
inner_result = execute(group_query, inner_key, inner_value)
|
33
|
-
results[new_key] = inner_result[inner_key]
|
34
|
-
end
|
35
|
-
results
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,51 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQL
|
4
|
-
module Groups
|
5
|
-
class LookaheadParser
|
6
|
-
def self.parse(base_selection)
|
7
|
-
LookaheadParser.new.group_selections(base_selection, {})
|
8
|
-
end
|
9
|
-
|
10
|
-
def group_selections(root, hash)
|
11
|
-
selections = root.selections
|
12
|
-
group_selections = selections.select { |selection| selection.field.is_a?(GraphQL::Groups::Schema::GroupField) }
|
13
|
-
group_selections.each do |selection|
|
14
|
-
own_query = get_field_proc(selection.field, selection.arguments)
|
15
|
-
hash[selection.name] ||= { proc: own_query }
|
16
|
-
hash[selection.name][:aggregates] = aggregates(selection)
|
17
|
-
end
|
18
|
-
group_selections
|
19
|
-
.filter { |selection| selection.selects?(:group_by) }
|
20
|
-
.each { |selection| hash[selection.name][:nested] = group_selections(selection.selection(:group_by), {}) }
|
21
|
-
hash
|
22
|
-
end
|
23
|
-
|
24
|
-
def get_field_proc(field, arguments)
|
25
|
-
# TODO: Use authorized instead of using send to circument protection
|
26
|
-
proc { |**kwargs| field.owner.send(:new, {}, nil).public_send(field.query_method, **arguments, **kwargs) }
|
27
|
-
end
|
28
|
-
|
29
|
-
def aggregates(group_selection)
|
30
|
-
aggregate_selections = group_selection.selections.select do |selection|
|
31
|
-
selection.field.is_a?(GraphQL::Groups::Schema::AggregateField)
|
32
|
-
end
|
33
|
-
aggregate_selections.each_with_object({}) do |selection, object|
|
34
|
-
name = selection.name
|
35
|
-
field = selection.field
|
36
|
-
if name == :count
|
37
|
-
proc = proc { |**kwargs| field.owner.send(:new, {}, nil).public_send(field.query_method, **kwargs) }
|
38
|
-
object[name] = { proc: proc }
|
39
|
-
elsif selection.field.own_attributes.present?
|
40
|
-
object[name] = { proc: get_aggregate_proc(field, selection.arguments), attributes: field.own_attributes }
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def get_aggregate_proc(field, arguments)
|
46
|
-
# TODO: Use authorized instead of using send to circument protection
|
47
|
-
proc { |**kwargs| field.owner.send(:new, {}, nil).send(field.query_method, **kwargs, **arguments) }
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|