graphql-groups 0.1.1 → 0.1.2
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/.gitignore +1 -1
- data/.rubocop.yml +2 -1
- data/Gemfile.lock +28 -22
- data/Meister.png +0 -0
- data/README.md +115 -12
- data/Rakefile +10 -3
- data/benchmark/benchmark.jpg +0 -0
- data/benchmark/benchmark.rb +101 -0
- data/benchmark/benchmark_schema.rb +53 -0
- data/graphql-groups.gemspec +6 -4
- data/lib/graphql/groups.rb +2 -6
- data/lib/graphql/groups/executor.rb +2 -1
- data/lib/graphql/groups/group_type_registry.rb +23 -0
- data/lib/graphql/groups/has_groups.rb +24 -10
- data/lib/graphql/groups/result_transformer.rb +29 -15
- data/lib/graphql/groups/schema/group_field.rb +1 -0
- data/lib/graphql/groups/schema/group_result_type.rb +1 -1
- data/lib/graphql/groups/schema/group_type.rb +1 -4
- data/lib/graphql/groups/version.rb +1 -1
- metadata +49 -17
- data/lib/graphql/groups/extensions/wrap.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46969188f84ff7921fee98bfad6f6264125bcce523dfcb7c93550eb1755a3951
|
|
4
|
+
data.tar.gz: f6a925a6c8037b42a134fca19d5d0d631656bdd3ec3a8a963fcfe95dbfbf16e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 450065ae2c4f015adc736241d1a917cdc65ceaa122305db706f6b138263cbbdf32c1636b7ad2017d058ff4c2276c970a4896e471015badbabbc5ee72aca3be01
|
|
7
|
+
data.tar.gz: 81da40cf05c3b6b284858037baee1b9402ef38eaeb4559315784e4fbfd2f6d8a5f7897dabbf981f4a5fd220d5ee66fb06dc89fd8e785202951ca0edb22d69f3b
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -7,11 +7,12 @@ Metrics/BlockLength:
|
|
|
7
7
|
ExcludedMethods: ['describe', 'context']
|
|
8
8
|
|
|
9
9
|
Layout/MultilineMethodCallIndentation:
|
|
10
|
-
EnforcedStyle:
|
|
10
|
+
EnforcedStyle: indented_relative_to_receiver
|
|
11
11
|
|
|
12
12
|
Style/BlockDelimiters:
|
|
13
13
|
Exclude:
|
|
14
14
|
- spec/**/*
|
|
15
|
+
- benchmark/**/*
|
|
15
16
|
|
|
16
17
|
RSpec/ExampleLength:
|
|
17
18
|
Exclude:
|
data/Gemfile.lock
CHANGED
|
@@ -7,27 +7,27 @@ PATH
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
|
-
activemodel (
|
|
11
|
-
activesupport (=
|
|
12
|
-
activerecord (
|
|
13
|
-
activemodel (=
|
|
14
|
-
activesupport (=
|
|
15
|
-
|
|
16
|
-
activesupport (5.2.3)
|
|
10
|
+
activemodel (6.0.3.2)
|
|
11
|
+
activesupport (= 6.0.3.2)
|
|
12
|
+
activerecord (6.0.3.2)
|
|
13
|
+
activemodel (= 6.0.3.2)
|
|
14
|
+
activesupport (= 6.0.3.2)
|
|
15
|
+
activesupport (6.0.3.2)
|
|
17
16
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
18
17
|
i18n (>= 0.7, < 2)
|
|
19
18
|
minitest (~> 5.1)
|
|
20
19
|
tzinfo (~> 1.1)
|
|
20
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
|
21
21
|
addressable (2.7.0)
|
|
22
22
|
public_suffix (>= 2.0.2, < 5.0)
|
|
23
|
-
arel (9.0.0)
|
|
24
23
|
ast (2.4.1)
|
|
24
|
+
benchmark-ips (2.8.2)
|
|
25
25
|
concurrent-ruby (1.1.6)
|
|
26
26
|
database_cleaner (1.8.5)
|
|
27
27
|
database_cleaner-active_record (1.8.0)
|
|
28
28
|
activerecord
|
|
29
29
|
database_cleaner (~> 1.8.0)
|
|
30
|
-
diff-lcs (1.
|
|
30
|
+
diff-lcs (1.4.4)
|
|
31
31
|
docile (1.3.2)
|
|
32
32
|
domain_name (0.5.20190701)
|
|
33
33
|
unf (>= 0.0.5, < 1.0.0)
|
|
@@ -35,8 +35,12 @@ GEM
|
|
|
35
35
|
hashie (~> 3.0)
|
|
36
36
|
http (> 0.8, < 3.0)
|
|
37
37
|
multi_json (~> 1)
|
|
38
|
-
graphql (1.
|
|
38
|
+
graphql (1.11.1)
|
|
39
|
+
gruff (0.10.0)
|
|
40
|
+
histogram
|
|
41
|
+
rmagick
|
|
39
42
|
hashie (3.6.0)
|
|
43
|
+
histogram (0.2.4.1)
|
|
40
44
|
http (2.2.2)
|
|
41
45
|
addressable (~> 2.3)
|
|
42
46
|
http-cookie (~> 1.0)
|
|
@@ -46,11 +50,10 @@ GEM
|
|
|
46
50
|
domain_name (~> 0.5)
|
|
47
51
|
http-form_data (1.0.3)
|
|
48
52
|
http_parser.rb (0.6.0)
|
|
49
|
-
i18n (1.8.
|
|
53
|
+
i18n (1.8.3)
|
|
50
54
|
concurrent-ruby (~> 1.0)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
multi_json (1.14.1)
|
|
55
|
+
minitest (5.14.1)
|
|
56
|
+
multi_json (1.15.0)
|
|
54
57
|
parallel (1.19.2)
|
|
55
58
|
parser (2.7.1.4)
|
|
56
59
|
ast (~> 2.4.1)
|
|
@@ -59,6 +62,7 @@ GEM
|
|
|
59
62
|
rake (13.0.1)
|
|
60
63
|
regexp_parser (1.7.1)
|
|
61
64
|
rexml (3.2.4)
|
|
65
|
+
rmagick (4.1.2)
|
|
62
66
|
rspec (3.9.0)
|
|
63
67
|
rspec-core (~> 3.9.0)
|
|
64
68
|
rspec-expectations (~> 3.9.0)
|
|
@@ -86,11 +90,10 @@ GEM
|
|
|
86
90
|
rubocop-rspec (1.42.0)
|
|
87
91
|
rubocop (>= 0.87.0)
|
|
88
92
|
ruby-progressbar (1.10.1)
|
|
89
|
-
simplecov (0.
|
|
93
|
+
simplecov (0.18.5)
|
|
90
94
|
docile (~> 1.1)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
simplecov-html (0.10.2)
|
|
95
|
+
simplecov-html (~> 0.11)
|
|
96
|
+
simplecov-html (0.12.2)
|
|
94
97
|
sqlite3 (1.4.2)
|
|
95
98
|
thread_safe (0.3.6)
|
|
96
99
|
tzinfo (1.2.7)
|
|
@@ -99,21 +102,24 @@ GEM
|
|
|
99
102
|
unf_ext
|
|
100
103
|
unf_ext (0.0.7.7)
|
|
101
104
|
unicode-display_width (1.7.0)
|
|
105
|
+
zeitwerk (2.4.0)
|
|
102
106
|
|
|
103
107
|
PLATFORMS
|
|
104
108
|
ruby
|
|
105
109
|
|
|
106
110
|
DEPENDENCIES
|
|
107
|
-
activerecord (~>
|
|
111
|
+
activerecord (~> 6.0)
|
|
112
|
+
benchmark-ips (~> 2.8)
|
|
108
113
|
bundler (~> 2.0)
|
|
109
|
-
database_cleaner-active_record
|
|
110
|
-
gqli
|
|
114
|
+
database_cleaner-active_record (~> 1.8)
|
|
115
|
+
gqli (~> 1.0)
|
|
111
116
|
graphql-groups!
|
|
117
|
+
gruff (~> 0.10)
|
|
112
118
|
rake (~> 13.0)
|
|
113
119
|
rspec (~> 3.0)
|
|
114
120
|
rubocop (~> 0.88)
|
|
115
121
|
rubocop-rspec (~> 1.42)
|
|
116
|
-
simplecov
|
|
122
|
+
simplecov (~> 0.18.5)
|
|
117
123
|
sqlite3 (~> 1.4.2)
|
|
118
124
|
|
|
119
125
|
BUNDLED WITH
|
data/Meister.png
ADDED
|
Binary file
|
data/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# GraphQL Groups
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/graphql-groups)
|
|
3
4
|
[](https://github.com/hschne/graphql-groups/workflows/Build/badge.svg)
|
|
4
5
|
[](https://codeclimate.com/github/hschne/graphql-groups/maintainability)
|
|
5
6
|
[](https://codeclimate.com/github/hschne/graphql-groups/test_coverage)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Statistics and aggregates built on top of [graphql-ruby](https://github.com/rmosolgo/graphql-ruby).
|
|
8
9
|
|
|
9
10
|
## Installation
|
|
10
11
|
|
|
11
|
-
Add this line to your application's Gemfile and
|
|
12
|
+
Add this line to your application's Gemfile and run `bundle install`.
|
|
12
13
|
|
|
13
14
|
```ruby
|
|
14
15
|
gem 'graphql-groups'
|
|
@@ -19,27 +20,28 @@ $ bundle install
|
|
|
19
20
|
|
|
20
21
|
## Usage
|
|
21
22
|
|
|
22
|
-
Create a new group type to specify which attributes you wish to group by inheriting from `GraphQL::Groups::GroupType
|
|
23
|
+
Create a new group type to specify which attributes you wish to group by inheriting from `GraphQL::Groups::GroupType`:
|
|
23
24
|
|
|
24
25
|
```ruby
|
|
25
26
|
class AuthorGroupType < GraphQL::Groups::GroupType
|
|
26
27
|
scope { Author.all }
|
|
27
28
|
|
|
29
|
+
by :name
|
|
28
30
|
by :age
|
|
29
31
|
end
|
|
30
32
|
```
|
|
31
33
|
|
|
32
|
-
Include the new type in your schema using the `group` keyword.
|
|
34
|
+
Include the new type in your schema using the `group` keyword, and you are done.
|
|
33
35
|
|
|
34
36
|
```ruby
|
|
35
|
-
class QueryType <
|
|
37
|
+
class QueryType < GraphQL::Schema::Object
|
|
36
38
|
include GraphQL::Groups
|
|
37
39
|
|
|
38
40
|
group :author_groups, AuthorGroupType
|
|
39
41
|
end
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
You can then run
|
|
44
|
+
You can then run a query to retrieve statistical information about your data, for example the number of authors per age.
|
|
43
45
|
|
|
44
46
|
```graphql
|
|
45
47
|
query myQuery{
|
|
@@ -67,14 +69,74 @@ query myQuery{
|
|
|
67
69
|
]
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
|
-
|
|
71
72
|
```
|
|
72
73
|
|
|
74
|
+
## Why?
|
|
75
|
+
|
|
76
|
+
`graphql-ruby` lacks a built in way to query statistical data of collections. It is possible to add this functionality by
|
|
77
|
+
using `group_by` (see for example [here](https://dev.to/gopeter/how-to-add-a-groupby-field-to-your-graphql-api-1f2j)),
|
|
78
|
+
but this performs poorly for large amounts of data.
|
|
79
|
+
|
|
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
|
+
|
|
73
83
|
## Advanced Usage
|
|
74
84
|
|
|
85
|
+
#### Grouping by Multiple Attributes
|
|
86
|
+
|
|
87
|
+
This library really shines when you want to group by multiple attributes, or otherwise retrieve complex statistical information
|
|
88
|
+
within a single GraphQL query.
|
|
89
|
+
|
|
90
|
+
For example, to get the number of authors grouped by their name, and then also by age, you could construct a query similar to this:
|
|
91
|
+
|
|
92
|
+
```graphql
|
|
93
|
+
query myQuery{
|
|
94
|
+
authorGroups {
|
|
95
|
+
name {
|
|
96
|
+
key
|
|
97
|
+
count
|
|
98
|
+
groupBy {
|
|
99
|
+
age {
|
|
100
|
+
key
|
|
101
|
+
count
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"authorGroups":{
|
|
112
|
+
"name":[
|
|
113
|
+
{
|
|
114
|
+
"key":"Ada",
|
|
115
|
+
"count":2,
|
|
116
|
+
"groupBy": {
|
|
117
|
+
"age": [
|
|
118
|
+
{
|
|
119
|
+
"key":"30",
|
|
120
|
+
"count":1
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"key":"35",
|
|
124
|
+
"count":1
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
...
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`graphql-groups` will automatically execute the required queries and return the results in a easily parsable response.
|
|
136
|
+
|
|
75
137
|
#### Custom Grouping Queries
|
|
76
138
|
|
|
77
|
-
To customize
|
|
139
|
+
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.
|
|
78
140
|
|
|
79
141
|
```ruby
|
|
80
142
|
class AuthorGroupType < GraphQL::Groups::Schema::GroupType
|
|
@@ -111,7 +173,32 @@ class BookGroupType < GraphQL::Groups::Schema::GroupType
|
|
|
111
173
|
end
|
|
112
174
|
```
|
|
113
175
|
|
|
114
|
-
|
|
176
|
+
When defining a group type's scope you may access the parents `object` and `context`.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
class QueryType < GraphQL::Schema::Object
|
|
180
|
+
field :statistics, StatisticsType, null: false
|
|
181
|
+
|
|
182
|
+
def statistics
|
|
183
|
+
Book.all
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class StatisticsType < GraphQL::Schema::Object
|
|
188
|
+
include GraphQL::Groups
|
|
189
|
+
|
|
190
|
+
group :books, BookGroupType
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
class BookGroupType < GraphQL::Groups::Schema::GroupType
|
|
194
|
+
# `object` refers to `Book.all`
|
|
195
|
+
scope { object.where(author_id: context[:current_person]) }
|
|
196
|
+
|
|
197
|
+
by :name
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
For more examples see the [feature spec](./spec/graphql/feature_spec.rb) and [test schema](./spec/graphql/support/test_schema.rb)
|
|
115
202
|
|
|
116
203
|
### Custom Aggregates
|
|
117
204
|
|
|
@@ -158,10 +245,26 @@ end
|
|
|
158
245
|
|
|
159
246
|
For more examples see the [feature spec](./spec/graphql/feature_spec.rb) and [test schema](./spec/graphql/support/test_schema)
|
|
160
247
|
|
|
248
|
+
## Performance
|
|
249
|
+
|
|
250
|
+
While it is possible to add grouping to your GraphQL schema by using `group_by` (see [above](#why)) this performs poorly for large amounts of data. The graph below shows the number of requests per second possible with both implementations.
|
|
251
|
+
|
|
252
|
+

|
|
253
|
+
|
|
254
|
+
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
|
+
|
|
256
|
+
Benchmarks are generated using [benchmark-ips](https://github.com/evanphx/benchmark-ips). The benchmark script used to generate the report be found [here](./benchmark/benchmark.rb)
|
|
257
|
+
|
|
161
258
|
## Limitations and Known Issues
|
|
162
259
|
|
|
163
|
-
*This gem is in early development
|
|
164
|
-
that this libraries API will not change fundamentally from one release to the next.
|
|
260
|
+
*This gem is in early development!* There are a number of issues that are still being addressed. There is no guarantee
|
|
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.
|
|
262
|
+
|
|
263
|
+
## Credits
|
|
264
|
+
|
|
265
|
+

|
|
266
|
+
|
|
267
|
+
graphql-groups is supported by and battle-tested at [Meister](https://www.meisterlabs.com/)
|
|
165
268
|
|
|
166
269
|
## Development
|
|
167
270
|
|
|
@@ -171,7 +274,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
171
274
|
|
|
172
275
|
## Contributing
|
|
173
276
|
|
|
174
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
277
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/hschne/graphql-groups. 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.
|
|
175
278
|
|
|
176
279
|
## License
|
|
177
280
|
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rspec/core/rake_task'
|
|
3
3
|
|
|
4
4
|
RSpec::Core::RakeTask.new(:spec)
|
|
5
5
|
|
|
6
|
-
task :
|
|
6
|
+
task default: :spec
|
|
7
|
+
|
|
8
|
+
desc 'Show benchmark results'
|
|
9
|
+
task :benchmark do
|
|
10
|
+
require_relative 'benchmark/benchmark'
|
|
11
|
+
|
|
12
|
+
Compare.new.run
|
|
13
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
data/graphql-groups.gemspec
CHANGED
|
@@ -29,15 +29,17 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
30
30
|
spec.require_paths = ['lib']
|
|
31
31
|
|
|
32
|
-
spec.add_development_dependency 'activerecord', '~>
|
|
32
|
+
spec.add_development_dependency 'activerecord', '~> 6.0'
|
|
33
|
+
spec.add_development_dependency 'benchmark-ips', '~> 2.8'
|
|
33
34
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
34
|
-
spec.add_development_dependency 'database_cleaner-active_record'
|
|
35
|
-
spec.add_development_dependency 'gqli'
|
|
35
|
+
spec.add_development_dependency 'database_cleaner-active_record', '~> 1.8'
|
|
36
|
+
spec.add_development_dependency 'gqli', '~> 1.0'
|
|
37
|
+
spec.add_development_dependency 'gruff', '~> 0.10'
|
|
36
38
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
37
39
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
38
40
|
spec.add_development_dependency 'rubocop', '~> 0.88'
|
|
39
41
|
spec.add_development_dependency 'rubocop-rspec', '~> 1.42'
|
|
40
|
-
spec.add_development_dependency 'simplecov'
|
|
42
|
+
spec.add_development_dependency 'simplecov', '~> 0.18.5'
|
|
41
43
|
spec.add_development_dependency 'sqlite3', '~> 1.4.2'
|
|
42
44
|
|
|
43
45
|
spec.add_dependency 'graphql', '~> 1', '> 1.9'
|
data/lib/graphql/groups.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require 'graphql/groups/version'
|
|
4
4
|
|
|
5
5
|
require 'graphql'
|
|
6
|
-
require 'graphql/groups/extensions/wrap'
|
|
7
6
|
|
|
7
|
+
require 'graphql/groups/group_type_registry'
|
|
8
8
|
require 'graphql/groups/schema/group_field'
|
|
9
9
|
require 'graphql/groups/schema/aggregate_field'
|
|
10
10
|
require 'graphql/groups/schema/aggregate_type'
|
|
@@ -28,15 +28,11 @@ module GraphQL
|
|
|
28
28
|
|
|
29
29
|
module ClassMethods
|
|
30
30
|
def group(name, type, **options)
|
|
31
|
-
# TODO: Suppress/warn if options are used that cannot be used
|
|
32
31
|
field name, type, extras: [:lookahead], null: false, **options
|
|
33
32
|
|
|
34
33
|
define_method name do |lookahead: nil|
|
|
35
34
|
execution_plan = GraphQL::Groups::LookaheadParser.parse(lookahead)
|
|
36
|
-
base_query =
|
|
37
|
-
type.instance_eval do
|
|
38
|
-
base_query = instance_eval(&@own_scope)
|
|
39
|
-
end
|
|
35
|
+
base_query = type.authorized_new(object, context).scope
|
|
40
36
|
results = Executor.call(base_query, execution_plan)
|
|
41
37
|
GraphQL::Groups::ResultTransformer.new.run(results)
|
|
42
38
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'singleton'
|
|
2
|
+
|
|
3
|
+
module GraphQL
|
|
4
|
+
module Groups
|
|
5
|
+
class GroupTypeRegistry
|
|
6
|
+
include Singleton
|
|
7
|
+
|
|
8
|
+
attr_reader :types
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@types = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def register(type, derived)
|
|
15
|
+
types[type] = derived
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def get(type)
|
|
19
|
+
types[type]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module GraphQL
|
|
3
4
|
module Groups
|
|
4
5
|
module HasGroups
|
|
@@ -6,7 +7,16 @@ module GraphQL
|
|
|
6
7
|
base.extend(ClassMethods)
|
|
7
8
|
end
|
|
8
9
|
|
|
10
|
+
attr_reader :scope
|
|
11
|
+
|
|
12
|
+
def initialize(object, context)
|
|
13
|
+
super(object, context)
|
|
14
|
+
@scope = instance_eval(&self.class.class_scope)
|
|
15
|
+
end
|
|
16
|
+
|
|
9
17
|
module ClassMethods
|
|
18
|
+
attr_reader :class_scope
|
|
19
|
+
|
|
10
20
|
# TODO: Error if there are no groupings defined
|
|
11
21
|
def by(name, **options, &block)
|
|
12
22
|
query_method = options[:query_method] || name
|
|
@@ -21,6 +31,7 @@ module GraphQL
|
|
|
21
31
|
kwargs[:scope].group(name)
|
|
22
32
|
end
|
|
23
33
|
|
|
34
|
+
# TODO: Warn / Disallow overwriting these resolver methods
|
|
24
35
|
define_method resolver_method do |**_|
|
|
25
36
|
group[name]
|
|
26
37
|
end
|
|
@@ -37,31 +48,34 @@ module GraphQL
|
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
def scope(&block)
|
|
40
|
-
@
|
|
51
|
+
@class_scope = block
|
|
41
52
|
end
|
|
42
53
|
|
|
43
54
|
private
|
|
44
55
|
|
|
45
56
|
def own_result_type
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
type = name.safe_constantize || GraphQL::Groups::Schema::GroupResultType
|
|
57
|
+
type = find_result_type
|
|
49
58
|
own_group_type = self
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
registry = GraphQL::Groups::GroupTypeRegistry.instance
|
|
61
|
+
# To avoid name conflicts check if a result type has already been registered, and if not create a new one
|
|
62
|
+
registry.get(type) || registry.register(type, Class.new(type) do
|
|
63
|
+
graphql_name type.name.demodulize
|
|
54
64
|
|
|
55
65
|
field :group_by, own_group_type, null: false, camelize: true
|
|
56
66
|
|
|
57
67
|
def group_by
|
|
58
68
|
group_result[1][:nested]
|
|
59
69
|
end
|
|
60
|
-
end
|
|
70
|
+
end)
|
|
61
71
|
end
|
|
62
72
|
|
|
63
|
-
def
|
|
64
|
-
@
|
|
73
|
+
def find_result_type
|
|
74
|
+
return @own_result_type if @own_result_type
|
|
75
|
+
|
|
76
|
+
return GraphQL::Groups::Schema::GroupResultType unless name
|
|
77
|
+
|
|
78
|
+
"#{name.gsub(/Type$/, '')}ResultType".safe_constantize || GraphQL::Groups::Schema::GroupResultType
|
|
65
79
|
end
|
|
66
80
|
end
|
|
67
81
|
end
|
|
@@ -24,24 +24,23 @@ module GraphQL
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def transform_result(key, result)
|
|
27
|
-
result.each_with_object({}) do |(aggregate_key,
|
|
28
|
-
if
|
|
29
|
-
|
|
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
|
|
30
32
|
else
|
|
31
|
-
object.deep_merge!(transform_aggregate(key, aggregate_key,
|
|
33
|
+
object.deep_merge!(transform_aggregate(key, aggregate_key, aggregate_value))
|
|
32
34
|
end
|
|
33
35
|
end
|
|
36
|
+
|
|
37
|
+
transformed.presence || { key => [] }
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
# TODO: Merge transform aggregate and transform attribute
|
|
37
41
|
def transform_aggregate(key, aggregate, result)
|
|
38
42
|
result.each_with_object({}) do |(keys, value), object|
|
|
39
|
-
|
|
40
|
-
keys = keys ? Array.wrap(keys).map { |x| x || 'null' } : ['null']
|
|
41
|
-
nested = [:nested] * (key.length - 1)
|
|
42
|
-
|
|
43
|
-
# See https://stackoverflow.com/a/5095149/2553104
|
|
44
|
-
with_zipped = key.zip(keys).zip(nested).flatten!.compact
|
|
43
|
+
with_zipped = build_keys(key, keys)
|
|
45
44
|
with_zipped.append(aggregate)
|
|
46
45
|
hash = with_zipped.reverse.inject(value) { |a, n| { n => a } }
|
|
47
46
|
object.deep_merge!(hash)
|
|
@@ -50,17 +49,32 @@ module GraphQL
|
|
|
50
49
|
|
|
51
50
|
def transform_attribute(key, aggregate, attribute, result)
|
|
52
51
|
result.each_with_object({}) do |(keys, value), object|
|
|
53
|
-
|
|
54
|
-
keys = keys ? Array.wrap(keys).map { |x| x || 'null' } : ['null']
|
|
55
|
-
nested = [:nested] * (key.length - 1)
|
|
56
|
-
|
|
57
|
-
with_zipped = key.zip(keys).zip(nested).flatten!.compact
|
|
52
|
+
with_zipped = build_keys(key, keys)
|
|
58
53
|
with_zipped.append(aggregate)
|
|
59
54
|
with_zipped.append(attribute)
|
|
60
55
|
hash = with_zipped.reverse.inject(value) { |a, n| { n => a } }
|
|
61
56
|
object.deep_merge!(hash)
|
|
62
57
|
end
|
|
63
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
|
|
77
|
+
end
|
|
64
78
|
end
|
|
65
79
|
end
|
|
66
80
|
end
|
|
@@ -10,10 +10,7 @@ module GraphQL
|
|
|
10
10
|
|
|
11
11
|
alias group object
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
super(object, context)
|
|
15
|
-
end
|
|
16
|
-
|
|
13
|
+
# TODO: Make group field inherit from default field, so that users default args/fields are respected
|
|
17
14
|
field_class(GroupField)
|
|
18
15
|
end
|
|
19
16
|
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.1.2
|
|
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: 2020-07-
|
|
11
|
+
date: 2020-07-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -16,14 +16,28 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '6.0'
|
|
20
20
|
type: :development
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: benchmark-ips
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.8'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.8'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: bundler
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -42,30 +56,44 @@ dependencies:
|
|
|
42
56
|
name: database_cleaner-active_record
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
44
58
|
requirements:
|
|
45
|
-
- - "
|
|
59
|
+
- - "~>"
|
|
46
60
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
61
|
+
version: '1.8'
|
|
48
62
|
type: :development
|
|
49
63
|
prerelease: false
|
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
65
|
requirements:
|
|
52
|
-
- - "
|
|
66
|
+
- - "~>"
|
|
53
67
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
68
|
+
version: '1.8'
|
|
55
69
|
- !ruby/object:Gem::Dependency
|
|
56
70
|
name: gqli
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
58
72
|
requirements:
|
|
59
|
-
- - "
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: gruff
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
60
88
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
89
|
+
version: '0.10'
|
|
62
90
|
type: :development
|
|
63
91
|
prerelease: false
|
|
64
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
93
|
requirements:
|
|
66
|
-
- - "
|
|
94
|
+
- - "~>"
|
|
67
95
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
96
|
+
version: '0.10'
|
|
69
97
|
- !ruby/object:Gem::Dependency
|
|
70
98
|
name: rake
|
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -126,16 +154,16 @@ dependencies:
|
|
|
126
154
|
name: simplecov
|
|
127
155
|
requirement: !ruby/object:Gem::Requirement
|
|
128
156
|
requirements:
|
|
129
|
-
- - "
|
|
157
|
+
- - "~>"
|
|
130
158
|
- !ruby/object:Gem::Version
|
|
131
|
-
version:
|
|
159
|
+
version: 0.18.5
|
|
132
160
|
type: :development
|
|
133
161
|
prerelease: false
|
|
134
162
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
163
|
requirements:
|
|
136
|
-
- - "
|
|
164
|
+
- - "~>"
|
|
137
165
|
- !ruby/object:Gem::Version
|
|
138
|
-
version:
|
|
166
|
+
version: 0.18.5
|
|
139
167
|
- !ruby/object:Gem::Dependency
|
|
140
168
|
name: sqlite3
|
|
141
169
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -188,14 +216,18 @@ files:
|
|
|
188
216
|
- Gemfile
|
|
189
217
|
- Gemfile.lock
|
|
190
218
|
- LICENSE.txt
|
|
219
|
+
- Meister.png
|
|
191
220
|
- README.md
|
|
192
221
|
- Rakefile
|
|
222
|
+
- benchmark/benchmark.jpg
|
|
223
|
+
- benchmark/benchmark.rb
|
|
224
|
+
- benchmark/benchmark_schema.rb
|
|
193
225
|
- bin/console
|
|
194
226
|
- bin/setup
|
|
195
227
|
- graphql-groups.gemspec
|
|
196
228
|
- lib/graphql/groups.rb
|
|
197
229
|
- lib/graphql/groups/executor.rb
|
|
198
|
-
- lib/graphql/groups/
|
|
230
|
+
- lib/graphql/groups/group_type_registry.rb
|
|
199
231
|
- lib/graphql/groups/has_aggregates.rb
|
|
200
232
|
- lib/graphql/groups/has_groups.rb
|
|
201
233
|
- lib/graphql/groups/lookahead_parser.rb
|