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.
- checksums.yaml +7 -0
- data/.github/workflows/build.yml +28 -0
- data/.gitignore +166 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +120 -0
- data/LICENSE.txt +21 -0
- data/README.md +182 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/graphql-groups.gemspec +44 -0
- data/lib/graphql/groups.rb +46 -0
- data/lib/graphql/groups/executor.rb +39 -0
- data/lib/graphql/groups/extensions/wrap.rb +11 -0
- data/lib/graphql/groups/has_aggregates.rb +58 -0
- data/lib/graphql/groups/has_groups.rb +69 -0
- data/lib/graphql/groups/lookahead_parser.rb +51 -0
- data/lib/graphql/groups/result_transformer.rb +66 -0
- data/lib/graphql/groups/schema/aggregate_field.rb +21 -0
- data/lib/graphql/groups/schema/aggregate_type.rb +26 -0
- data/lib/graphql/groups/schema/group_field.rb +16 -0
- data/lib/graphql/groups/schema/group_result_type.rb +34 -0
- data/lib/graphql/groups/schema/group_type.rb +21 -0
- data/lib/graphql/groups/version.rb +5 -0
- metadata +235 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
class LookaheadParser
|
6
|
+
def self.parse(base_selection)
|
7
|
+
LookaheadParser.new.group_selections(base_selection, {})
|
8
|
+
end
|
9
|
+
|
10
|
+
def group_selections(root, hash)
|
11
|
+
selections = root.selections
|
12
|
+
group_selections = selections.select { |selection| selection.field.is_a?(GraphQL::Groups::Schema::GroupField) }
|
13
|
+
group_selections.each do |selection|
|
14
|
+
own_query = get_field_proc(selection.field, selection.arguments)
|
15
|
+
hash[selection.name] ||= { proc: own_query }
|
16
|
+
hash[selection.name][:aggregates] = aggregates(selection)
|
17
|
+
end
|
18
|
+
group_selections
|
19
|
+
.filter { |selection| selection.selects?(:group_by) }
|
20
|
+
.each { |selection| hash[selection.name][:nested] = group_selections(selection.selection(:group_by), {}) }
|
21
|
+
hash
|
22
|
+
end
|
23
|
+
|
24
|
+
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) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def aggregates(group_selection)
|
30
|
+
aggregate_selections = group_selection.selections.select do |selection|
|
31
|
+
selection.field.is_a?(GraphQL::Groups::Schema::AggregateField)
|
32
|
+
end
|
33
|
+
aggregate_selections.each_with_object({}) do |selection, object|
|
34
|
+
name = selection.name
|
35
|
+
field = selection.field
|
36
|
+
if name == :count
|
37
|
+
proc = proc { |**kwargs| field.owner.send(:new, {}, nil).public_send(field.query_method, **kwargs) }
|
38
|
+
object[name] = { proc: proc }
|
39
|
+
elsif selection.field.own_attributes.present?
|
40
|
+
object[name] = { proc: get_aggregate_proc(field, selection.arguments), attributes: field.own_attributes }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
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) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
class ResultTransformer
|
6
|
+
def run(results)
|
7
|
+
transform_results(results)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
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.
|
15
|
+
#
|
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.
|
19
|
+
#
|
20
|
+
# It all makes a lot more sense if you look at the GraphQL interface for statistics :)
|
21
|
+
#
|
22
|
+
# We accomplish this by transforming each result set to a hash and then merging them into a single one.
|
23
|
+
results.each_with_object({}) { |(key, value), object| object.deep_merge!(transform_result(key, value)) }
|
24
|
+
end
|
25
|
+
|
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)) }
|
30
|
+
else
|
31
|
+
object.deep_merge!(transform_aggregate(key, aggregate_key, value))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# TODO: Merge transform aggregate and transform attribute
|
37
|
+
def transform_aggregate(key, aggregate, result)
|
38
|
+
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
|
45
|
+
with_zipped.append(aggregate)
|
46
|
+
hash = with_zipped.reverse.inject(value) { |a, n| { n => a } }
|
47
|
+
object.deep_merge!(hash)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def transform_attribute(key, aggregate, attribute, result)
|
52
|
+
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
|
58
|
+
with_zipped.append(aggregate)
|
59
|
+
with_zipped.append(attribute)
|
60
|
+
hash = with_zipped.reverse.inject(value) { |a, n| { n => a } }
|
61
|
+
object.deep_merge!(hash)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
module Schema
|
6
|
+
class AggregateField < GraphQL::Schema::Field
|
7
|
+
attr_reader :own_attributes, :query_method
|
8
|
+
|
9
|
+
def initialize(query_method:, **kwargs, &definition_block)
|
10
|
+
@query_method = query_method
|
11
|
+
super(**kwargs, &definition_block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def attribute(attribute)
|
15
|
+
@own_attributes ||= []
|
16
|
+
@own_attributes += Array.wrap(attribute)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Groups
|
7
|
+
module Schema
|
8
|
+
class AggregateType < GraphQL::Schema::Object
|
9
|
+
alias aggregate object
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def add_fields(fields)
|
13
|
+
fields.each do |attribute|
|
14
|
+
resolve_method = "resolve_#{attribute}".to_sym
|
15
|
+
field attribute, Float, null: false, resolver_method: resolve_method
|
16
|
+
|
17
|
+
define_method resolve_method do
|
18
|
+
object[attribute]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
module Schema
|
6
|
+
class GroupField < GraphQL::Schema::Field
|
7
|
+
attr_reader :query_method
|
8
|
+
|
9
|
+
def initialize(query_method:, **options, &definition_block)
|
10
|
+
@query_method = query_method
|
11
|
+
super(**options, &definition_block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
require 'graphql/groups/schema/group_type'
|
5
|
+
|
6
|
+
module GraphQL
|
7
|
+
module Groups
|
8
|
+
module Schema
|
9
|
+
class GroupResultType < GraphQL::Schema::Object
|
10
|
+
include HasAggregates
|
11
|
+
|
12
|
+
alias group_result object
|
13
|
+
|
14
|
+
field :key, String, null: false
|
15
|
+
|
16
|
+
field :count, Integer, null: false
|
17
|
+
|
18
|
+
aggregate_field :count, Integer, null: false, query_method: :count, resolver_method: :resolve_count
|
19
|
+
|
20
|
+
def key
|
21
|
+
group_result[0]
|
22
|
+
end
|
23
|
+
|
24
|
+
def count(scope:, **_)
|
25
|
+
scope.size
|
26
|
+
end
|
27
|
+
|
28
|
+
def resolve_count
|
29
|
+
group_result[1][:count]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphql'
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Groups
|
7
|
+
module Schema
|
8
|
+
class GroupType < GraphQL::Schema::Object
|
9
|
+
include HasGroups
|
10
|
+
|
11
|
+
alias group object
|
12
|
+
|
13
|
+
def initialize(object, context)
|
14
|
+
super(object, context)
|
15
|
+
end
|
16
|
+
|
17
|
+
field_class(GroupField)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphql-groups
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hans-Jörg Schnedlitz
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: database_cleaner-active_record
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: gqli
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '13.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '13.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.88'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.88'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.42'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.42'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: sqlite3
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 1.4.2
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 1.4.2
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: graphql
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '1'
|
160
|
+
- - ">"
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '1.9'
|
163
|
+
type: :runtime
|
164
|
+
prerelease: false
|
165
|
+
version_requirements: !ruby/object:Gem::Requirement
|
166
|
+
requirements:
|
167
|
+
- - "~>"
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '1'
|
170
|
+
- - ">"
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '1.9'
|
173
|
+
description: "GraphQL Groups makes it easy to add aggregation queries to your GraphQL
|
174
|
+
schema. It combines a simple, flexible \nschema definition with high performance'\n"
|
175
|
+
email:
|
176
|
+
- hans.schnedlitz@gmail.com
|
177
|
+
executables: []
|
178
|
+
extensions: []
|
179
|
+
extra_rdoc_files: []
|
180
|
+
files:
|
181
|
+
- ".github/workflows/build.yml"
|
182
|
+
- ".gitignore"
|
183
|
+
- ".rspec"
|
184
|
+
- ".rubocop.yml"
|
185
|
+
- ".travis.yml"
|
186
|
+
- CHANGELOG.md
|
187
|
+
- CODE_OF_CONDUCT.md
|
188
|
+
- Gemfile
|
189
|
+
- Gemfile.lock
|
190
|
+
- LICENSE.txt
|
191
|
+
- README.md
|
192
|
+
- Rakefile
|
193
|
+
- bin/console
|
194
|
+
- bin/setup
|
195
|
+
- graphql-groups.gemspec
|
196
|
+
- lib/graphql/groups.rb
|
197
|
+
- lib/graphql/groups/executor.rb
|
198
|
+
- lib/graphql/groups/extensions/wrap.rb
|
199
|
+
- lib/graphql/groups/has_aggregates.rb
|
200
|
+
- lib/graphql/groups/has_groups.rb
|
201
|
+
- lib/graphql/groups/lookahead_parser.rb
|
202
|
+
- lib/graphql/groups/result_transformer.rb
|
203
|
+
- lib/graphql/groups/schema/aggregate_field.rb
|
204
|
+
- lib/graphql/groups/schema/aggregate_type.rb
|
205
|
+
- lib/graphql/groups/schema/group_field.rb
|
206
|
+
- lib/graphql/groups/schema/group_result_type.rb
|
207
|
+
- lib/graphql/groups/schema/group_type.rb
|
208
|
+
- lib/graphql/groups/version.rb
|
209
|
+
homepage: https://github.com/hschne/graphql-groups
|
210
|
+
licenses:
|
211
|
+
- MIT
|
212
|
+
metadata:
|
213
|
+
homepage_uri: https://github.com/hschne/graphql-groups
|
214
|
+
source_code_uri: https://github.com/hschne/graphql-groups
|
215
|
+
changelog_uri: https://github.com/hschne/graphql-groups/blob/master/CHANGELOG.md
|
216
|
+
post_install_message:
|
217
|
+
rdoc_options: []
|
218
|
+
require_paths:
|
219
|
+
- lib
|
220
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
221
|
+
requirements:
|
222
|
+
- - ">="
|
223
|
+
- !ruby/object:Gem::Version
|
224
|
+
version: '0'
|
225
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - ">="
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0'
|
230
|
+
requirements: []
|
231
|
+
rubygems_version: 3.0.3
|
232
|
+
signing_key:
|
233
|
+
specification_version: 4
|
234
|
+
summary: Create flexible and performant aggregation queries with graphql-ruby
|
235
|
+
test_files: []
|