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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46969188f84ff7921fee98bfad6f6264125bcce523dfcb7c93550eb1755a3951
4
- data.tar.gz: f6a925a6c8037b42a134fca19d5d0d631656bdd3ec3a8a963fcfe95dbfbf16e9
3
+ metadata.gz: c30a036d32f2580c7dbbbd4ee10d8cc364e8dc4af0e92fc5d0ab1d3c8378f55e
4
+ data.tar.gz: fcd1bb1beeaed5247eba94cb72ac3418d3f29ca19a839ff78ed83e1b86aa602b
5
5
  SHA512:
6
- metadata.gz: 450065ae2c4f015adc736241d1a917cdc65ceaa122305db706f6b138263cbbdf32c1636b7ad2017d058ff4c2276c970a4896e471015badbabbc5ee72aca3be01
7
- data.tar.gz: 81da40cf05c3b6b284858037baee1b9402ef38eaeb4559315784e4fbfd2f6d8a5f7897dabbf981f4a5fd220d5ee66fb06dc89fd8e785202951ca0edb22d69f3b
6
+ metadata.gz: 8bbde25d65f689d6b2127761e1b535d1d1946bdedd569707498e4516d33d69da76b7efc9d34bb9bd41212b6822f195188c39556a88ad0d85a75e6eb34fbce93c
7
+ data.tar.gz: 43acc2931f89122b3e23f20e181cb8ab9e02d979bfd9153554d1f72b4a27a81a9ae722b975342e9ab37e5a930b2a2b777af97af8249ad74bfad293c93efbbf62
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql-groups (0.1.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
- Statistics and aggregates built on top of [graphql-ruby](https://github.com/rmosolgo/graphql-ruby).
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
- Create a new group type to specify which attributes you wish to group by inheriting from `GraphQL::Groups::GroupType`:
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 :author_groups, AuthorGroupType
39
+ group :author_group_by, AuthorGroupType
41
40
  end
42
41
  ```
43
42
 
44
- You can then run a query to retrieve statistical information about your data, for example the number of authors per age.
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
- authorGroups {
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
- "authorGroups":{
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 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.
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
- #### Grouping by Multiple Attributes
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
- #### Custom Grouping Queries
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
- ![Meister](meister.png)
281
+ <a href="https://www.meisterlabs.com"><img src="Meister.png" width="50%"></a>
266
282
 
267
- graphql-groups is supported by and battle-tested at [Meister](https://www.meisterlabs.com/)
283
+ [graphql-groups](https://github.com/hschne/graphql-groups) was created at [meister](https://www.meisterlabs.com/)
268
284
 
269
285
  ## Development
270
286
 
@@ -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 performant aggregation queries with graphql-ruby'
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'
@@ -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
- LookaheadParser.new.group_selections(base_selection, {})
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
- # 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) }
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
- # TODO: Use authorized instead of using send to circument protection
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 results
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 that
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 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
+ 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
- # TODO: Merge transform aggregate and transform attribute
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
- result.each_with_object({}) do |(keys, value), object|
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
- hash = with_zipped.reverse.inject(value) { |a, n| { n => a } }
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
- result.each_with_object({}) do |(keys, value), object|
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
- hash = with_zipped.reverse.inject(value) { |a, n| { n => a } }
56
- object.deep_merge!(hash)
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)
@@ -7,7 +7,6 @@ module GraphQL
7
7
  attr_reader :query_method
8
8
 
9
9
  def initialize(query_method:, **options, &definition_block)
10
- # TODO: Make sure that users can access the context in custom query methods
11
10
  @query_method = query_method
12
11
  super(**options, &definition_block)
13
12
  end
@@ -1,5 +1,5 @@
1
1
  module Graphql
2
2
  module Groups
3
- VERSION = '0.1.2'.freeze
3
+ VERSION = '0.1.3'.freeze
4
4
  end
5
5
  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.2
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-07-30 00:00:00.000000000 Z
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 performant aggregation queries with graphql-ruby
266
+ summary: Create flexible and fast aggregation queries with graphql-ruby
267
267
  test_files: []