graphql-groups 0.1.1 → 0.1.2

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: 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