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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 335a39d1b463c58fea371465f9f5bc97034b6e698b4345acb106e107be5339a5
4
- data.tar.gz: 8a605ac9caf825e1a45c1dd385a7de295d98876b8147ddeed63d0eaad6c56ef4
3
+ metadata.gz: 46969188f84ff7921fee98bfad6f6264125bcce523dfcb7c93550eb1755a3951
4
+ data.tar.gz: f6a925a6c8037b42a134fca19d5d0d631656bdd3ec3a8a963fcfe95dbfbf16e9
5
5
  SHA512:
6
- metadata.gz: 884ec7cb95a187e714920bfd214cbde487fb1aa1787fad8595f16a2294df68ba705074630ebfc54169bb99e4e8a09f0dc30e9e6d68530413d5fa45af6a6dd718
7
- data.tar.gz: 719ea4cbd49fff5274b2e90cd4f3b7e041d50a8026139b32092d97f39b448b63d30a94208ad76e6ce17a258ca4de64641fb5fc5cff4677fe021a18f12b15da47
6
+ metadata.gz: 450065ae2c4f015adc736241d1a917cdc65ceaa122305db706f6b138263cbbdf32c1636b7ad2017d058ff4c2276c970a4896e471015badbabbc5ee72aca3be01
7
+ data.tar.gz: 81da40cf05c3b6b284858037baee1b9402ef38eaeb4559315784e4fbfd2f6d8a5f7897dabbf981f4a5fd220d5ee66fb06dc89fd8e785202951ca0edb22d69f3b
data/.gitignore CHANGED
@@ -162,5 +162,5 @@ tags
162
162
 
163
163
  .tool-versions
164
164
  .rspec_status
165
- test.db
165
+ test.db*
166
166
  coverage
@@ -7,11 +7,12 @@ Metrics/BlockLength:
7
7
  ExcludedMethods: ['describe', 'context']
8
8
 
9
9
  Layout/MultilineMethodCallIndentation:
10
- EnforcedStyle: indented
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:
@@ -7,27 +7,27 @@ PATH
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (5.2.3)
11
- activesupport (= 5.2.3)
12
- activerecord (5.2.3)
13
- activemodel (= 5.2.3)
14
- activesupport (= 5.2.3)
15
- arel (>= 9.0)
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.3)
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.10.10)
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.2)
53
+ i18n (1.8.3)
50
54
  concurrent-ruby (~> 1.0)
51
- json (2.3.0)
52
- minitest (5.14.0)
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.16.1)
93
+ simplecov (0.18.5)
90
94
  docile (~> 1.1)
91
- json (>= 1.8, < 3)
92
- simplecov-html (~> 0.10.0)
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 (~> 5.0)
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
Binary file
data/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # GraphQL Groups
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/graphql-groups.svg)](https://badge.fury.io/rb/graphql-groups)
3
4
  [![Build Status](https://github.com/hschne/graphql-groups/workflows/Build/badge.svg)](https://github.com/hschne/graphql-groups/workflows/Build/badge.svg)
4
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/692d4125ac8548fb145e/maintainability)](https://codeclimate.com/github/hschne/graphql-groups/maintainability)
5
6
  [![Test Coverage](https://api.codeclimate.com/v1/badges/692d4125ac8548fb145e/test_coverage)](https://codeclimate.com/github/hschne/graphql-groups/test_coverage)
6
7
 
7
- Create flexible and performant aggregation queries with [graphql-ruby](https://github.com/rmosolgo/graphql-ruby)
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 execute bundler.
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 < BaseType
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 an aggregation query for this grouping.
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 how items are grouped, you may specify the grouping query by creating a method of the same name in the group type.
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
- For more examples see the [feature spec](./spec/graphql/feature_spec.rb) and [test schema](./spec/graphql/support/test_schema)
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
+ ![benchmark](benchmark/benchmark.jpg)
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!*. There are a number of issues that are still being addressed. There is no guarantee
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
+ ![Meister](meister.png)
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/[USERNAME]/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.
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 "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
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
@@ -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', '~> 5.0'
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'
@@ -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 = nil
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
@@ -24,7 +24,8 @@ module GraphQL
24
24
  end
25
25
  end
26
26
 
27
- return { key => results } unless value[:nested]
27
+ results = { key => results }
28
+ return results unless value[:nested]
28
29
 
29
30
  value[:nested].each do |inner_key, inner_value|
30
31
  new_key = (Array.wrap(key) << inner_key)
@@ -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
- @own_scope = block
51
+ @class_scope = block
41
52
  end
42
53
 
43
54
  private
44
55
 
45
56
  def own_result_type
46
- name = "#{self.name.gsub(/Type$/, '')}ResultType"
47
-
48
- type = name.safe_constantize || GraphQL::Groups::Schema::GroupResultType
57
+ type = find_result_type
49
58
  own_group_type = self
50
59
 
51
- @classes ||= {}
52
- @classes[type] ||= Class.new(type) do
53
- graphql_name name
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 own_scope
64
- @own_scope ||= nil
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, value), object|
28
- if value.values.any? { |x| x.is_a?(Hash) }
29
- value.each { |attribute, value| object.deep_merge!(transform_attribute(key, aggregate_key, attribute, value)) }
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, value))
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
- key = Array.wrap(key)
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
- key = Array.wrap(key)
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
@@ -7,6 +7,7 @@ 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
10
11
  @query_method = query_method
11
12
  super(**options, &definition_block)
12
13
  end
@@ -11,7 +11,7 @@ module GraphQL
11
11
 
12
12
  alias group_result object
13
13
 
14
- field :key, String, null: false
14
+ field :key, String, null: true
15
15
 
16
16
  field :count, Integer, null: false
17
17
 
@@ -10,10 +10,7 @@ module GraphQL
10
10
 
11
11
  alias group object
12
12
 
13
- def initialize(object, context)
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
@@ -1,5 +1,5 @@
1
1
  module Graphql
2
2
  module Groups
3
- VERSION = '0.1.1'.freeze
3
+ VERSION = '0.1.2'.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.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-14 00:00:00.000000000 Z
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: '5.0'
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: '5.0'
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: '0'
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: '0'
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: '0'
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: '0'
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/extensions/wrap.rb
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
@@ -1,11 +0,0 @@
1
- class Array
2
- def self.wrap(object)
3
- if object.nil?
4
- []
5
- elsif object.respond_to?(:to_ary)
6
- object.to_ary || [object]
7
- else
8
- [object]
9
- end
10
- end
11
- end