graphql-groups 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +10 -1
- data/README.md +37 -21
- data/graphql-groups.gemspec +1 -1
- data/lib/graphql/groups.rb +1 -1
- data/lib/graphql/groups/lookahead_parser.rb +9 -6
- data/lib/graphql/groups/result_transformer.rb +35 -15
- data/lib/graphql/groups/schema/group_field.rb +0 -1
- data/lib/graphql/groups/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c30a036d32f2580c7dbbbd4ee10d8cc364e8dc4af0e92fc5d0ab1d3c8378f55e
|
4
|
+
data.tar.gz: fcd1bb1beeaed5247eba94cb72ac3418d3f29ca19a839ff78ed83e1b86aa602b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8bbde25d65f689d6b2127761e1b535d1d1946bdedd569707498e4516d33d69da76b7efc9d34bb9bd41212b6822f195188c39556a88ad0d85a75e6eb34fbce93c
|
7
|
+
data.tar.gz: 43acc2931f89122b3e23f20e181cb8ab9e02d979bfd9153554d1f72b4a27a81a9ae722b975342e9ab37e5a930b2a2b777af97af8249ad74bfad293c93efbbf62
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
graphql-groups (0.1.
|
4
|
+
graphql-groups (0.1.3)
|
5
5
|
graphql (~> 1, > 1.9)
|
6
6
|
|
7
7
|
GEM
|
@@ -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.1.0)
|
40
|
+
activesupport (>= 5)
|
39
41
|
gruff (0.10.0)
|
40
42
|
histogram
|
41
43
|
rmagick
|
@@ -89,12 +91,15 @@ GEM
|
|
89
91
|
parser (>= 2.7.0.1)
|
90
92
|
rubocop-rspec (1.42.0)
|
91
93
|
rubocop (>= 0.87.0)
|
94
|
+
ruby-prof (1.4.1)
|
92
95
|
ruby-progressbar (1.10.1)
|
93
96
|
simplecov (0.18.5)
|
94
97
|
docile (~> 1.1)
|
95
98
|
simplecov-html (~> 0.11)
|
96
99
|
simplecov-html (0.12.2)
|
97
100
|
sqlite3 (1.4.2)
|
101
|
+
stackprof (0.2.15)
|
102
|
+
test-prof (0.12.0)
|
98
103
|
thread_safe (0.3.6)
|
99
104
|
tzinfo (1.2.7)
|
100
105
|
thread_safe (~> 0.1)
|
@@ -114,13 +119,17 @@ DEPENDENCIES
|
|
114
119
|
database_cleaner-active_record (~> 1.8)
|
115
120
|
gqli (~> 1.0)
|
116
121
|
graphql-groups!
|
122
|
+
groupdate
|
117
123
|
gruff (~> 0.10)
|
118
124
|
rake (~> 13.0)
|
119
125
|
rspec (~> 3.0)
|
120
126
|
rubocop (~> 0.88)
|
121
127
|
rubocop-rspec (~> 1.42)
|
128
|
+
ruby-prof
|
122
129
|
simplecov (~> 0.18.5)
|
123
130
|
sqlite3 (~> 1.4.2)
|
131
|
+
stackprof
|
132
|
+
test-prof
|
124
133
|
|
125
134
|
BUNDLED WITH
|
126
135
|
2.1.4
|
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
|
@@ -262,9 +278,9 @@ that this libraries API will not change fundamentally from one release to the ne
|
|
262
278
|
|
263
279
|
## Credits
|
264
280
|
|
265
|
-
|
281
|
+
<a href="https://www.meisterlabs.com"><img src="Meister.png" width="50%"></a>
|
266
282
|
|
267
|
-
graphql-groups
|
283
|
+
[graphql-groups](https://github.com/hschne/graphql-groups) was created at [meister](https://www.meisterlabs.com/)
|
268
284
|
|
269
285
|
## Development
|
270
286
|
|
data/graphql-groups.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.authors = ['Hans-Jörg Schnedlitz']
|
9
9
|
spec.email = ['hans.schnedlitz@gmail.com']
|
10
10
|
|
11
|
-
spec.summary = 'Create flexible and
|
11
|
+
spec.summary = 'Create flexible and fast aggregation queries with graphql-ruby'
|
12
12
|
spec.description = <<~HEREDOC
|
13
13
|
GraphQL Groups makes it easy to add aggregation queries to your GraphQL schema. It combines a simple, flexible
|
14
14
|
schema definition with high performance'
|
data/lib/graphql/groups.rb
CHANGED
@@ -31,7 +31,7 @@ module GraphQL
|
|
31
31
|
field name, type, extras: [:lookahead], null: false, **options
|
32
32
|
|
33
33
|
define_method name do |lookahead: nil|
|
34
|
-
execution_plan = GraphQL::Groups::LookaheadParser.parse(lookahead)
|
34
|
+
execution_plan = GraphQL::Groups::LookaheadParser.parse(lookahead, context)
|
35
35
|
base_query = type.authorized_new(object, context).scope
|
36
36
|
results = Executor.call(base_query, execution_plan)
|
37
37
|
GraphQL::Groups::ResultTransformer.new.run(results)
|
@@ -3,8 +3,13 @@
|
|
3
3
|
module GraphQL
|
4
4
|
module Groups
|
5
5
|
class LookaheadParser
|
6
|
-
def self.parse(base_selection)
|
7
|
-
|
6
|
+
def self.parse(base_selection, context)
|
7
|
+
# TODO: Raise error if no aggregate selection is made
|
8
|
+
LookaheadParser.new(context).group_selections(base_selection, {})
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(context)
|
12
|
+
@context = context
|
8
13
|
end
|
9
14
|
|
10
15
|
def group_selections(root, hash)
|
@@ -22,8 +27,7 @@ module GraphQL
|
|
22
27
|
end
|
23
28
|
|
24
29
|
def get_field_proc(field, arguments)
|
25
|
-
|
26
|
-
proc { |**kwargs| field.owner.send(:new, {}, nil).public_send(field.query_method, **arguments, **kwargs) }
|
30
|
+
proc { |**kwargs| field.owner.authorized_new(nil, @context).public_send(field.query_method, **arguments, **kwargs) }
|
27
31
|
end
|
28
32
|
|
29
33
|
def aggregates(group_selection)
|
@@ -43,8 +47,7 @@ module GraphQL
|
|
43
47
|
end
|
44
48
|
|
45
49
|
def get_aggregate_proc(field, arguments)
|
46
|
-
|
47
|
-
proc { |**kwargs| field.owner.send(:new, {}, nil).send(field.query_method, **kwargs, **arguments) }
|
50
|
+
proc { |**kwargs| field.owner.authorized_new(nil, @context).send(field.query_method, **kwargs, **arguments) }
|
48
51
|
end
|
49
52
|
end
|
50
53
|
end
|
@@ -10,12 +10,12 @@ module GraphQL
|
|
10
10
|
private
|
11
11
|
|
12
12
|
def transform_results(results)
|
13
|
-
# Because group query returns its results in a way that is not usable by GraphQL we need to transform these
|
14
|
-
# and merge them into a single dataset.
|
13
|
+
# Because group query returns its results in a way that is not usable by GraphQL we need to transform these
|
14
|
+
# results and merge them into a single dataset.
|
15
15
|
#
|
16
16
|
# The result of a group query usually is a hash where they keys are the values of the columns that were grouped
|
17
|
-
# and the values are the aggregates. What we want is a deep hash where each level contains the statistics for
|
18
|
-
# level in regards to the parent level.
|
17
|
+
# and the values are the aggregates. What we want is a deep hash where each level contains the statistics for
|
18
|
+
# that level in regards to the parent level.
|
19
19
|
#
|
20
20
|
# It all makes a lot more sense if you look at the GraphQL interface for statistics :)
|
21
21
|
#
|
@@ -24,11 +24,11 @@ module GraphQL
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def transform_result(key, result)
|
27
|
+
is_aggregate_result = result.values[0].values[0].is_a?(Hash)
|
28
|
+
|
27
29
|
transformed = result.each_with_object({}) do |(aggregate_key, aggregate_value), object|
|
28
|
-
if
|
29
|
-
aggregate_value
|
30
|
-
object.deep_merge!(transform_attribute(key, aggregate_key, attribute, value))
|
31
|
-
end
|
30
|
+
if is_aggregate_result
|
31
|
+
transform_aggregate_result(aggregate_key, aggregate_value, key, object)
|
32
32
|
else
|
33
33
|
object.deep_merge!(transform_aggregate(key, aggregate_key, aggregate_value))
|
34
34
|
end
|
@@ -37,24 +37,44 @@ module GraphQL
|
|
37
37
|
transformed.presence || { key => [] }
|
38
38
|
end
|
39
39
|
|
40
|
-
|
40
|
+
def transform_aggregate_result(aggregate_key, aggregate_value, key, object)
|
41
|
+
aggregate_value.each do |attribute, value|
|
42
|
+
object.deep_merge!(transform_attribute(key, aggregate_key, attribute, value))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
41
46
|
def transform_aggregate(key, aggregate, result)
|
42
|
-
|
47
|
+
return {} unless result.present?
|
48
|
+
|
49
|
+
hashes = result.map do |(keys, value)|
|
43
50
|
with_zipped = build_keys(key, keys)
|
44
51
|
with_zipped.append(aggregate)
|
45
|
-
|
46
|
-
object.deep_merge!(hash)
|
52
|
+
with_zipped.reverse.inject(value) { |a, n| { n => a } }
|
47
53
|
end
|
54
|
+
|
55
|
+
merge(hashes)
|
48
56
|
end
|
49
57
|
|
50
58
|
def transform_attribute(key, aggregate, attribute, result)
|
51
|
-
|
59
|
+
return {} unless result.present?
|
60
|
+
|
61
|
+
hashes = result.map do |(keys, value)|
|
52
62
|
with_zipped = build_keys(key, keys)
|
53
63
|
with_zipped.append(aggregate)
|
54
64
|
with_zipped.append(attribute)
|
55
|
-
|
56
|
-
|
65
|
+
with_zipped.reverse.inject(value) { |a, n| { n => a } }
|
66
|
+
end
|
67
|
+
merge(hashes)
|
68
|
+
end
|
69
|
+
|
70
|
+
def merge(hashes)
|
71
|
+
root_key = hashes.first.keys.first
|
72
|
+
result = hashes.each_with_object({}) do |hash, object|
|
73
|
+
inner = hash[root_key]
|
74
|
+
inner_key = inner.keys.first
|
75
|
+
object[inner_key] = inner.values.first
|
57
76
|
end
|
77
|
+
{ root_key => result }
|
58
78
|
end
|
59
79
|
|
60
80
|
def build_keys(key, keys)
|
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.3
|
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-
|
11
|
+
date: 2020-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -263,5 +263,5 @@ requirements: []
|
|
263
263
|
rubygems_version: 3.0.3
|
264
264
|
signing_key:
|
265
265
|
specification_version: 4
|
266
|
-
summary: Create flexible and
|
266
|
+
summary: Create flexible and fast aggregation queries with graphql-ruby
|
267
267
|
test_files: []
|