graphql-groups 0.1.1

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.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Hans-Jörg Schnedlitz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # GraphQL Groups
2
+
3
+ [![Build Status](https://github.com/hschne/graphql-groups/workflows/Build/badge.svg)](https://github.com/hschne/graphql-groups/workflows/Build/badge.svg)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/692d4125ac8548fb145e/maintainability)](https://codeclimate.com/github/hschne/graphql-groups/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/692d4125ac8548fb145e/test_coverage)](https://codeclimate.com/github/hschne/graphql-groups/test_coverage)
6
+
7
+ Create flexible and performant aggregation queries with [graphql-ruby](https://github.com/rmosolgo/graphql-ruby)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile and execute bundler.
12
+
13
+ ```ruby
14
+ gem 'graphql-groups'
15
+ ```
16
+ ```bash
17
+ $ bundle install
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ Create a new group type to specify which attributes you wish to group by inheriting from `GraphQL::Groups::GroupType`
23
+
24
+ ```ruby
25
+ class AuthorGroupType < GraphQL::Groups::GroupType
26
+ scope { Author.all }
27
+
28
+ by :age
29
+ end
30
+ ```
31
+
32
+ Include the new type in your schema using the `group` keyword.
33
+
34
+ ```ruby
35
+ class QueryType < BaseType
36
+ include GraphQL::Groups
37
+
38
+ group :author_groups, AuthorGroupType
39
+ end
40
+ ```
41
+
42
+ You can then run an aggregation query for this grouping.
43
+
44
+ ```graphql
45
+ query myQuery{
46
+ authorGroups {
47
+ age {
48
+ key
49
+ count
50
+ }
51
+ }
52
+ }
53
+ ```
54
+ ```json
55
+ {
56
+ "authorGroups":{
57
+ "age":[
58
+ {
59
+ "key":"31",
60
+ "count":1
61
+ },
62
+ {
63
+ "key":"35",
64
+ "count":3
65
+ },
66
+ ...
67
+ ]
68
+ }
69
+ }
70
+
71
+ ```
72
+
73
+ ## Advanced Usage
74
+
75
+ #### Custom Grouping Queries
76
+
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.
78
+
79
+ ```ruby
80
+ class AuthorGroupType < GraphQL::Groups::Schema::GroupType
81
+ scope { Author.all }
82
+
83
+ by :age
84
+
85
+ def age(scope:)
86
+ scope.group("(cast(age/10 as int) * 10) || '-' || ((cast(age/10 as int) + 1) * 10)")
87
+ end
88
+ end
89
+ ```
90
+
91
+ You may also pass arguments to custom grouping queries. In this case, pass any arguments to your group query as keyword arguments.
92
+
93
+ ```ruby
94
+ class BookGroupType < GraphQL::Groups::Schema::GroupType
95
+ scope { Book.all }
96
+
97
+ by :published_at do
98
+ argument :interval, String, required: false
99
+ end
100
+
101
+ def published_at(scope:, interval: nil)
102
+ case interval
103
+ when 'month'
104
+ scope.group("strftime('%Y-%m-01 00:00:00 UTC', published_at)")
105
+ when 'year'
106
+ scope.group("strftime('%Y-01-01 00:00:00 UTC', published_at)")
107
+ else
108
+ scope.group("strftime('%Y-%m-%d 00:00:00 UTC', published_at)")
109
+ end
110
+ end
111
+ end
112
+ ```
113
+
114
+ For more examples see the [feature spec](./spec/graphql/feature_spec.rb) and [test schema](./spec/graphql/support/test_schema)
115
+
116
+ ### Custom Aggregates
117
+
118
+ Per default `graphql-groups` supports aggregating `count` out of the box. If you need to other aggregates, such as sum or average
119
+ you may add them to your schema by creating a custom `GroupResultType`. Wire this up to your schema by specifying the result type in your
120
+ group type.
121
+
122
+ ```ruby
123
+ class AuthorGroupResultType < GraphQL::Groups::Schema::GroupResultType
124
+ aggregate :average do
125
+ attribute :age
126
+ end
127
+ end
128
+ ```
129
+
130
+ ```ruby
131
+ class AuthorGroupType < GraphQL::Groups::Schema::GroupType
132
+ scope { Author.all }
133
+
134
+ result_type { AuthorGroupResultType }
135
+
136
+ by :name
137
+ end
138
+ ```
139
+
140
+ Per default, the aggregate name and attribute will be used to construct the underlying aggregation query. The example above creates
141
+ ```ruby
142
+ scope.average(:age)
143
+ ```
144
+
145
+ If you need more control over how to aggregate you may define a custom query by creating a method matching the aggregate name. The method *must* take the keyword arguments `scope` and `attribute`.
146
+
147
+ ```ruby
148
+ class AuthorGroupResultType < GraphQL::Groups::Schema::GroupResultType
149
+ aggregate :average do
150
+ attribute :age
151
+ end
152
+
153
+ def average(scope:, attribute:)
154
+ scope.average(attribute)
155
+ end
156
+ end
157
+ ```
158
+
159
+ For more examples see the [feature spec](./spec/graphql/feature_spec.rb) and [test schema](./spec/graphql/support/test_schema)
160
+
161
+ ## Limitations and Known Issues
162
+
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.
165
+
166
+ ## Development
167
+
168
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
169
+
170
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
171
+
172
+ ## Contributing
173
+
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.
175
+
176
+ ## License
177
+
178
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
179
+
180
+ ## Code of Conduct
181
+
182
+ Everyone interacting in the Graphql::Groups project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/graphql-groups/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "graphql/groups"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,44 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'graphql/groups/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'graphql-groups'
7
+ spec.version = Graphql::Groups::VERSION
8
+ spec.authors = ['Hans-Jörg Schnedlitz']
9
+ spec.email = ['hans.schnedlitz@gmail.com']
10
+
11
+ spec.summary = 'Create flexible and performant aggregation queries with graphql-ruby'
12
+ spec.description = <<~HEREDOC
13
+ GraphQL Groups makes it easy to add aggregation queries to your GraphQL schema. It combines a simple, flexible
14
+ schema definition with high performance'
15
+ HEREDOC
16
+ spec.homepage = 'https://github.com/hschne/graphql-groups'
17
+ spec.license = 'MIT'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/hschne/graphql-groups'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/hschne/graphql-groups/blob/master/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_development_dependency 'activerecord', '~> 5.0'
33
+ spec.add_development_dependency 'bundler', '~> 2.0'
34
+ spec.add_development_dependency 'database_cleaner-active_record'
35
+ spec.add_development_dependency 'gqli'
36
+ spec.add_development_dependency 'rake', '~> 13.0'
37
+ spec.add_development_dependency 'rspec', '~> 3.0'
38
+ spec.add_development_dependency 'rubocop', '~> 0.88'
39
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.42'
40
+ spec.add_development_dependency 'simplecov'
41
+ spec.add_development_dependency 'sqlite3', '~> 1.4.2'
42
+
43
+ spec.add_dependency 'graphql', '~> 1', '> 1.9'
44
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql/groups/version'
4
+
5
+ require 'graphql'
6
+ require 'graphql/groups/extensions/wrap'
7
+
8
+ require 'graphql/groups/schema/group_field'
9
+ require 'graphql/groups/schema/aggregate_field'
10
+ require 'graphql/groups/schema/aggregate_type'
11
+
12
+ require 'graphql/groups/has_aggregates'
13
+ require 'graphql/groups/has_groups'
14
+
15
+ require 'graphql/groups/schema/group_result_type'
16
+ require 'graphql/groups/schema/group_type'
17
+
18
+ require 'graphql/groups/lookahead_parser'
19
+ require 'graphql/groups/result_transformer'
20
+ require 'graphql/groups/executor'
21
+
22
+
23
+ module GraphQL
24
+ module Groups
25
+ def self.included(base)
26
+ base.extend ClassMethods
27
+ end
28
+
29
+ module ClassMethods
30
+ def group(name, type, **options)
31
+ # TODO: Suppress/warn if options are used that cannot be used
32
+ field name, type, extras: [:lookahead], null: false, **options
33
+
34
+ define_method name do |lookahead: nil|
35
+ 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
40
+ results = Executor.call(base_query, execution_plan)
41
+ GraphQL::Groups::ResultTransformer.new.run(results)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Groups
5
+ class Executor
6
+ class << self
7
+ def call(base_query, execution_plan)
8
+ execution_plan.each_with_object({}) do |(key, value), object|
9
+ object.merge!(execute(base_query, key, value))
10
+ end
11
+ end
12
+
13
+ def execute(scope, key, value)
14
+ group_query = value[:proc].call(scope: scope)
15
+ results = value[:aggregates].each_with_object({}) do |(aggregate_key, aggregate), object|
16
+ if aggregate_key == :count
17
+ object[:count] = aggregate[:proc].call(scope: group_query)
18
+ else
19
+ object[aggregate_key] ||= {}
20
+ aggregate[:attributes].each do |attribute|
21
+ result = aggregate[:proc].call(scope: group_query, attribute: attribute)
22
+ object[aggregate_key][attribute] = result
23
+ end
24
+ end
25
+ end
26
+
27
+ return { key => results } unless value[:nested]
28
+
29
+ value[:nested].each do |inner_key, inner_value|
30
+ new_key = (Array.wrap(key) << inner_key)
31
+ inner_result = execute(group_query, inner_key, inner_value)
32
+ results[new_key] = inner_result[inner_key]
33
+ end
34
+ results
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Groups
5
+ module HasAggregates
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def aggregate(name, *_, **options, &block)
12
+ aggregate_type = aggregate_type(name)
13
+
14
+ # TODO: Handle method name conflicts, or no query method found error
15
+ resolve_method = "resolve_#{name}".to_sym
16
+ query_method = options[:query_method] || name
17
+ field = aggregate_field name, aggregate_type,
18
+ null: false,
19
+ query_method: query_method,
20
+ resolver_method: resolve_method,
21
+ **options, &block
22
+ aggregate_type.add_fields(field.own_attributes)
23
+
24
+ # TODO: Avoid overwriting existing method
25
+ define_method query_method do |**kwargs|
26
+ scope = kwargs[:scope]
27
+ attribute = kwargs[:attribute]
28
+ scope.public_send(name, attribute)
29
+ end
30
+
31
+ define_method resolve_method do
32
+ group_result[1][name]
33
+ end
34
+ end
35
+
36
+ def aggregate_field(*args, **kwargs, &block)
37
+ field_defn = Schema::AggregateField.from_options(*args, owner: self, **kwargs, &block)
38
+ add_field(field_defn)
39
+ field_defn
40
+ end
41
+
42
+ private
43
+
44
+ def aggregate_type(name)
45
+ # TODO: Handle no aggregate type found
46
+ name = "#{name}AggregateType".upcase_first
47
+ own_aggregate_types[name] ||= Class.new(Schema::AggregateType) do
48
+ graphql_name name
49
+ end
50
+ end
51
+
52
+ def own_aggregate_types
53
+ @own_aggregate_types ||= {}
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Groups
4
+ module HasGroups
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ # TODO: Error if there are no groupings defined
11
+ def by(name, **options, &block)
12
+ query_method = options[:query_method] || name
13
+ resolver_method = "resolve_#{query_method}".to_sym
14
+ group_field name, [own_result_type],
15
+ null: false,
16
+ resolver_method: resolver_method,
17
+ query_method: query_method,
18
+ **options, &block
19
+
20
+ define_method query_method do |**kwargs|
21
+ kwargs[:scope].group(name)
22
+ end
23
+
24
+ define_method resolver_method do |**_|
25
+ group[name]
26
+ end
27
+ end
28
+
29
+ def group_field(*args, **kwargs, &block)
30
+ field_defn = Schema::GroupField.from_options(*args, owner: self, **kwargs, &block)
31
+ add_field(field_defn)
32
+ field_defn
33
+ end
34
+
35
+ def result_type(&block)
36
+ @own_result_type = instance_eval(&block)
37
+ end
38
+
39
+ def scope(&block)
40
+ @own_scope = block
41
+ end
42
+
43
+ private
44
+
45
+ def own_result_type
46
+ name = "#{self.name.gsub(/Type$/, '')}ResultType"
47
+
48
+ type = name.safe_constantize || GraphQL::Groups::Schema::GroupResultType
49
+ own_group_type = self
50
+
51
+ @classes ||= {}
52
+ @classes[type] ||= Class.new(type) do
53
+ graphql_name name
54
+
55
+ field :group_by, own_group_type, null: false, camelize: true
56
+
57
+ def group_by
58
+ group_result[1][:nested]
59
+ end
60
+ end
61
+ end
62
+
63
+ def own_scope
64
+ @own_scope ||= nil
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end