graphql-groups 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -1
- data/README.md +2 -3
- data/graphql-groups.gemspec +1 -0
- data/lib/graphql/groups.rb +8 -6
- data/lib/graphql/groups/group_type_registry.rb +6 -0
- data/lib/graphql/groups/has_aggregates.rb +2 -7
- data/lib/graphql/groups/has_groups.rb +19 -6
- data/lib/graphql/groups/pending_query.rb +22 -0
- data/lib/graphql/groups/query_builder.rb +96 -0
- data/lib/graphql/groups/query_builder_context.rb +21 -0
- data/lib/graphql/groups/query_result.rb +17 -0
- data/lib/graphql/groups/result_transformer.rb +31 -77
- data/lib/graphql/groups/schema/group_result_type.rb +1 -1
- data/lib/graphql/groups/schema/group_type.rb +1 -2
- data/lib/graphql/groups/utils.rb +49 -0
- data/lib/graphql/groups/version.rb +1 -1
- metadata +21 -4
- data/lib/graphql/groups/executor.rb +0 -40
- data/lib/graphql/groups/lookahead_parser.rb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d719d15bde5e54f543072d6b188540e38ef8eb1635c156af4f09a1b769fe59b0
|
4
|
+
data.tar.gz: 6e8b3a42ff5605c0a4fd99f4581982d4e52860cc51776a354881302c9eabd408
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c99e16857d3c8450469a3e1ff18ac6ec38e4921108b689413a34b0bccf96f4682ab9827678129d6938188fd956fc93bf95ddab2b00490a630fbb6a48bc803349
|
7
|
+
data.tar.gz: 6f0464eff3da15b38bdab1ff27dda8e0131fa72936b567fdd1ad20a5e9fc7e2a390a05ef07afaa2e169dbd4c382cb41904e442f2ac41442df45f174fb045a79c
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
graphql-groups (0.
|
4
|
+
graphql-groups (0.2.0)
|
5
5
|
graphql (~> 1, > 1.9)
|
6
6
|
|
7
7
|
GEM
|
@@ -36,6 +36,8 @@ GEM
|
|
36
36
|
http (> 0.8, < 3.0)
|
37
37
|
multi_json (~> 1)
|
38
38
|
graphql (1.11.1)
|
39
|
+
groupdate (5.2.1)
|
40
|
+
activesupport (>= 5)
|
39
41
|
gruff (0.10.0)
|
40
42
|
histogram
|
41
43
|
rmagick
|
@@ -114,6 +116,7 @@ DEPENDENCIES
|
|
114
116
|
database_cleaner-active_record (~> 1.8)
|
115
117
|
gqli (~> 1.0)
|
116
118
|
graphql-groups!
|
119
|
+
groupdate (~> 5.2.1)
|
117
120
|
gruff (~> 0.10)
|
118
121
|
rake (~> 13.0)
|
119
122
|
rspec (~> 3.0)
|
data/README.md
CHANGED
@@ -269,12 +269,11 @@ While it is possible to add grouping to your GraphQL schema by using `group_by`
|
|
269
269
|
|
270
270
|
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.
|
271
271
|
|
272
|
-
Benchmarks
|
272
|
+
Benchmarks can be generated by running `rake benchmark`. The benchmark script used to generate the report be found [here](./benchmark/benchmark.rb)
|
273
273
|
|
274
274
|
## Limitations and Known Issues
|
275
275
|
|
276
|
-
|
277
|
-
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.
|
276
|
+
Please refer to the [issue tracker](https://github.com/hschne/graphql-groups/issues) for a list of known issues.
|
278
277
|
|
279
278
|
## Credits
|
280
279
|
|
data/graphql-groups.gemspec
CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
|
|
36
36
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
37
37
|
spec.add_development_dependency 'database_cleaner-active_record', '~> 1.8'
|
38
38
|
spec.add_development_dependency 'gqli', '~> 1.0'
|
39
|
+
spec.add_development_dependency 'groupdate', '~> 5.2.1'
|
39
40
|
spec.add_development_dependency 'gruff', '~> 0.10'
|
40
41
|
spec.add_development_dependency 'rake', '~> 13.0'
|
41
42
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
data/lib/graphql/groups.rb
CHANGED
@@ -4,6 +4,7 @@ require 'graphql/groups/version'
|
|
4
4
|
|
5
5
|
require 'graphql'
|
6
6
|
|
7
|
+
require 'graphql/groups/utils'
|
7
8
|
require 'graphql/groups/group_type_registry'
|
8
9
|
require 'graphql/groups/schema/group_field'
|
9
10
|
require 'graphql/groups/schema/aggregate_field'
|
@@ -15,9 +16,11 @@ require 'graphql/groups/has_groups'
|
|
15
16
|
require 'graphql/groups/schema/group_result_type'
|
16
17
|
require 'graphql/groups/schema/group_type'
|
17
18
|
|
18
|
-
require 'graphql/groups/
|
19
|
+
require 'graphql/groups/query_result'
|
20
|
+
require 'graphql/groups/pending_query'
|
21
|
+
require 'graphql/groups/query_builder_context'
|
22
|
+
require 'graphql/groups/query_builder'
|
19
23
|
require 'graphql/groups/result_transformer'
|
20
|
-
require 'graphql/groups/executor'
|
21
24
|
|
22
25
|
|
23
26
|
module GraphQL
|
@@ -31,10 +34,9 @@ module GraphQL
|
|
31
34
|
field name, type, extras: [:lookahead], null: false, **options
|
32
35
|
|
33
36
|
define_method name do |lookahead: nil|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
GraphQL::Groups::ResultTransformer.new.run(results)
|
37
|
+
pending_queries = QueryBuilder.parse(lookahead, object, context)
|
38
|
+
query_results = pending_queries.map(&:execute)
|
39
|
+
GraphQL::Groups::ResultTransformer.new.run(query_results)
|
38
40
|
end
|
39
41
|
end
|
40
42
|
end
|
@@ -11,7 +11,6 @@ module GraphQL
|
|
11
11
|
def aggregate(name, *_, **options, &block)
|
12
12
|
aggregate_type = aggregate_type(name)
|
13
13
|
|
14
|
-
# TODO: Handle method name conflicts, or no query method found error
|
15
14
|
resolve_method = "resolve_#{name}".to_sym
|
16
15
|
query_method = options[:query_method] || name
|
17
16
|
field = aggregate_field name, aggregate_type,
|
@@ -21,11 +20,8 @@ module GraphQL
|
|
21
20
|
**options, &block
|
22
21
|
aggregate_type.add_fields(field.own_attributes)
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
scope = kwargs[:scope]
|
27
|
-
attribute = kwargs[:attribute]
|
28
|
-
scope.public_send(name, attribute)
|
23
|
+
define_method query_method do |scope:, **kwargs|
|
24
|
+
scope.public_send(name, **kwargs)
|
29
25
|
end
|
30
26
|
|
31
27
|
define_method resolve_method do
|
@@ -42,7 +38,6 @@ module GraphQL
|
|
42
38
|
private
|
43
39
|
|
44
40
|
def aggregate_type(name)
|
45
|
-
# TODO: Handle no aggregate type found
|
46
41
|
name = "#{name}AggregateType".upcase_first
|
47
42
|
own_aggregate_types[name] ||= Class.new(Schema::AggregateType) do
|
48
43
|
graphql_name name
|
@@ -17,28 +17,27 @@ module GraphQL
|
|
17
17
|
module ClassMethods
|
18
18
|
attr_reader :class_scope
|
19
19
|
|
20
|
-
# TODO: Error if there are no groupings defined
|
21
20
|
def by(name, **options, &block)
|
22
21
|
query_method = options[:query_method] || name
|
23
22
|
resolver_method = "resolve_#{query_method}".to_sym
|
24
23
|
group_field name, [own_result_type],
|
25
|
-
null: false,
|
26
|
-
resolver_method: resolver_method,
|
27
24
|
query_method: query_method,
|
25
|
+
null: true,
|
26
|
+
resolver_method: resolver_method,
|
27
|
+
|
28
28
|
**options, &block
|
29
29
|
|
30
30
|
define_method query_method do |**kwargs|
|
31
31
|
kwargs[:scope].group(name)
|
32
32
|
end
|
33
33
|
|
34
|
-
# TODO: Warn / Disallow overwriting these resolver methods
|
35
34
|
define_method resolver_method do |**_|
|
36
35
|
group[name]
|
37
36
|
end
|
38
37
|
end
|
39
38
|
|
40
39
|
def group_field(*args, **kwargs, &block)
|
41
|
-
field_defn =
|
40
|
+
field_defn = field_class.from_options(*args, owner: self, **kwargs, &block)
|
42
41
|
add_field(field_defn)
|
43
42
|
field_defn
|
44
43
|
end
|
@@ -65,7 +64,21 @@ module GraphQL
|
|
65
64
|
field :group_by, own_group_type, null: false, camelize: true
|
66
65
|
|
67
66
|
def group_by
|
68
|
-
group_result[1][:
|
67
|
+
group_result[1][:group_by]
|
68
|
+
end
|
69
|
+
end)
|
70
|
+
end
|
71
|
+
|
72
|
+
def own_field_type
|
73
|
+
type = "#{name}Field"
|
74
|
+
base_field_type = field_class
|
75
|
+
registry = GraphQL::Groups::GroupTypeRegistry.instance
|
76
|
+
registry.get(type) || registry.register(type, Class.new(base_field_type) do
|
77
|
+
attr_reader :query_method
|
78
|
+
|
79
|
+
def initialize(query_method:, **options, &definition_block)
|
80
|
+
@query_method = query_method
|
81
|
+
super(**options.except(:query_method), &definition_block)
|
69
82
|
end
|
70
83
|
end)
|
71
84
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
class PendingQuery
|
6
|
+
attr_reader :key
|
7
|
+
attr_reader :aggregate
|
8
|
+
attr_reader :query
|
9
|
+
|
10
|
+
def initialize(key, aggregate, proc)
|
11
|
+
@key = Utils.wrap(key)
|
12
|
+
@aggregate = Utils.wrap(aggregate)
|
13
|
+
@query = proc
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute
|
17
|
+
result = @query.call
|
18
|
+
QueryResult.new(@key, @aggregate, result)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
class QueryBuilder
|
6
|
+
def self.parse(lookahead, object, context)
|
7
|
+
QueryBuilder.new(lookahead, object, context).group_selections
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(lookahead, object, context)
|
11
|
+
@lookahead = lookahead
|
12
|
+
@context = context
|
13
|
+
type = @lookahead.field.type.of_type
|
14
|
+
@base_query = proc { type.authorized_new(object, context).scope }
|
15
|
+
super()
|
16
|
+
end
|
17
|
+
|
18
|
+
def group_selections(lookahead = @lookahead, current_context = QueryBuilderContext.new([], @base_query))
|
19
|
+
selections = lookahead.selections
|
20
|
+
group_field_type = lookahead.field.type.of_type.field_class
|
21
|
+
group_selections = selections.select { |selection| selection.field.is_a?(group_field_type) }
|
22
|
+
queries = group_selections.each_with_object([]) do |selection, object|
|
23
|
+
field_proc = proc_from_selection(selection.field, selection.arguments)
|
24
|
+
context = current_context.update(selection.name, field_proc)
|
25
|
+
object << create_pending_queries(selection, context)
|
26
|
+
end
|
27
|
+
nested_queries = group_selections
|
28
|
+
.filter { |selection| selection.selects?(:group_by) }
|
29
|
+
.each_with_object([]) do |selection, object|
|
30
|
+
field_proc = proc_from_selection(selection.field, selection.arguments)
|
31
|
+
context = current_context.update(selection.name, field_proc)
|
32
|
+
object << group_selections(selection.selection(:group_by), context)
|
33
|
+
end
|
34
|
+
(queries + nested_queries).flatten
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def create_pending_queries(current_selection, context)
|
40
|
+
aggregate_selections = current_selection
|
41
|
+
.selections
|
42
|
+
.select { |selection| selection.field.is_a?(GraphQL::Groups::Schema::AggregateField) }
|
43
|
+
count_queries = count_queries(aggregate_selections, context)
|
44
|
+
aggregate_queries = aggregate_queries(aggregate_selections, context)
|
45
|
+
(count_queries + aggregate_queries)
|
46
|
+
end
|
47
|
+
|
48
|
+
def count_queries(aggregate_selections, context)
|
49
|
+
# TODO: When getting multiple aggregates for the same base data we could do just a single query instead of many
|
50
|
+
aggregate_selections
|
51
|
+
.select { |selection| selection.name == :count }
|
52
|
+
.map do |selection|
|
53
|
+
field = selection.field
|
54
|
+
count_proc = proc { |scope| field.owner.send(:new, {}, nil).public_send(field.query_method, scope: scope) }
|
55
|
+
combined = combine_procs(context.current_proc, count_proc)
|
56
|
+
PendingQuery.new(context.grouping, selection.name, combined)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def aggregate_queries(aggregate_selections, context)
|
61
|
+
aggregate_selections
|
62
|
+
.select { |selection| selection.field.own_attributes.present? }
|
63
|
+
.map { |selection| attribute_queries(context, selection) }
|
64
|
+
.flatten
|
65
|
+
end
|
66
|
+
|
67
|
+
def attribute_queries(context, selection)
|
68
|
+
selection.field
|
69
|
+
.own_attributes
|
70
|
+
.select { |attribute| selection.selections.map(&:name).include?(attribute) }
|
71
|
+
.map do |attribute|
|
72
|
+
aggregate_proc = proc_from_attribute(selection.field, attribute, selection.arguments)
|
73
|
+
combined = combine_procs(context.current_proc, aggregate_proc)
|
74
|
+
PendingQuery.new(context.grouping, [selection.name, attribute], combined)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def combine_procs(base_proc, new_proc)
|
79
|
+
proc { new_proc.call(base_proc.call) }
|
80
|
+
end
|
81
|
+
|
82
|
+
def proc_from_selection(field, arguments)
|
83
|
+
proc { |scope| field.owner.authorized_new(nil, @context).public_send(field.query_method, scope: scope, **arguments) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def proc_from_attribute(field, attribute, arguments)
|
87
|
+
proc do |scope|
|
88
|
+
field.owner.authorized_new(nil, @context)
|
89
|
+
.public_send(field.query_method,
|
90
|
+
scope: scope,
|
91
|
+
attribute: attribute, **arguments)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class QueryBuilderContext
|
4
|
+
attr_reader :grouping
|
5
|
+
attr_reader :current_proc
|
6
|
+
|
7
|
+
def initialize(groupings = [], current_proc = nil)
|
8
|
+
@grouping = groupings
|
9
|
+
@current_proc = current_proc
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(grouping, new_proc)
|
13
|
+
new_grouping = @grouping + [grouping]
|
14
|
+
combined_proc = combine_procs(@current_proc, new_proc)
|
15
|
+
QueryBuilderContext.new(new_grouping, combined_proc)
|
16
|
+
end
|
17
|
+
|
18
|
+
def combine_procs(base_proc, new_proc)
|
19
|
+
proc { new_proc.call(base_proc.call) }
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
class QueryResult
|
6
|
+
attr_reader :key
|
7
|
+
attr_reader :aggregate
|
8
|
+
attr_reader :result_hash
|
9
|
+
|
10
|
+
def initialize(key, aggregate, result)
|
11
|
+
@key = Utils.wrap(key)
|
12
|
+
@aggregate = Utils.wrap(aggregate)
|
13
|
+
@result_hash = result
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -3,93 +3,47 @@
|
|
3
3
|
module GraphQL
|
4
4
|
module Groups
|
5
5
|
class ResultTransformer
|
6
|
-
def run(
|
7
|
-
|
6
|
+
def run(query_results)
|
7
|
+
# Sort by key length so that deeper nested queries come later
|
8
|
+
query_results
|
9
|
+
.sort_by { |query_result| query_result.key.length }
|
10
|
+
.each_with_object({}) do |query_result, object|
|
11
|
+
transform_result(query_result, object)
|
12
|
+
end
|
8
13
|
end
|
9
14
|
|
10
15
|
private
|
11
16
|
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
is_aggregate_result = result.values[0].values[0].is_a?(Hash)
|
28
|
-
|
29
|
-
transformed = result.each_with_object({}) do |(aggregate_key, aggregate_value), object|
|
30
|
-
if is_aggregate_result
|
31
|
-
transform_aggregate_result(aggregate_key, aggregate_value, key, object)
|
17
|
+
def transform_result(query_result, object)
|
18
|
+
keys = query_result.key
|
19
|
+
return object[keys[0]] = [] if query_result.result_hash.empty?
|
20
|
+
|
21
|
+
query_result.result_hash.each do |grouping_result|
|
22
|
+
group_result_keys = Utils.wrap(grouping_result[0])
|
23
|
+
group_result_value = grouping_result[1]
|
24
|
+
Utils.duplicate(keys, group_result_keys)
|
25
|
+
inner_hash = create_nested_result(keys, group_result_keys, object)
|
26
|
+
if query_result.aggregate.length == 1
|
27
|
+
inner_hash[query_result.aggregate[0]] = group_result_value
|
32
28
|
else
|
33
|
-
|
29
|
+
aggregate_type = query_result.aggregate[0]
|
30
|
+
aggregate_attribute = query_result.aggregate[1]
|
31
|
+
inner_hash[aggregate_type] ||= {}
|
32
|
+
inner_hash[aggregate_type][aggregate_attribute] ||= group_result_value
|
34
33
|
end
|
35
34
|
end
|
36
|
-
|
37
|
-
transformed.presence || { key => [] }
|
38
|
-
end
|
39
|
-
|
40
|
-
def transform_aggregate_result(aggregate_key, aggregate_value, key, object)
|
41
|
-
aggregate_value.each do |attribute, value|
|
42
|
-
object.deep_merge!(transform_attribute(key, aggregate_key, attribute, value))
|
43
|
-
end
|
44
35
|
end
|
45
36
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
merge(hashes)
|
56
|
-
end
|
37
|
+
def create_nested_result(keys, group_result_keys, object)
|
38
|
+
head_key, *rest_keys = keys
|
39
|
+
head_group_key, *rest_group_keys = group_result_keys
|
40
|
+
object[head_key] ||= {}
|
41
|
+
object[head_key][head_group_key] ||= {}
|
42
|
+
inner_hash = object[head_key][head_group_key]
|
43
|
+
return inner_hash if rest_keys.empty?
|
57
44
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
hashes = result.map do |(keys, value)|
|
62
|
-
with_zipped = build_keys(key, keys)
|
63
|
-
with_zipped.append(aggregate)
|
64
|
-
with_zipped.append(attribute)
|
65
|
-
with_zipped.reverse.inject(value) { |a, n| { n => a } }
|
66
|
-
end
|
67
|
-
merge(hashes)
|
68
|
-
end
|
69
|
-
|
70
|
-
def merge(hashes)
|
71
|
-
hashes.each_with_object({}) do |hash, object|
|
72
|
-
object.deep_merge!(hash)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def build_keys(key, keys)
|
77
|
-
key = wrap(key)
|
78
|
-
keys = keys ? wrap(keys) : [nil]
|
79
|
-
nested = [:nested] * (key.length - 1)
|
80
|
-
|
81
|
-
with_zipped = key.zip(keys).zip(nested).flatten!
|
82
|
-
with_zipped.first(with_zipped.size - 1)
|
83
|
-
end
|
84
|
-
|
85
|
-
def wrap(object)
|
86
|
-
if object.nil?
|
87
|
-
[]
|
88
|
-
elsif object.respond_to?(:to_ary)
|
89
|
-
object.to_ary || [object]
|
90
|
-
else
|
91
|
-
[object]
|
92
|
-
end
|
45
|
+
inner_hash[:group_by] ||= {}
|
46
|
+
create_nested_result(rest_keys, rest_group_keys, inner_hash[:group_by])
|
93
47
|
end
|
94
48
|
end
|
95
49
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Groups
|
5
|
+
module Utils
|
6
|
+
class << self
|
7
|
+
def wrap(object)
|
8
|
+
if object.nil?
|
9
|
+
[]
|
10
|
+
elsif object.respond_to?(:to_ary)
|
11
|
+
object.to_ary || [object]
|
12
|
+
else
|
13
|
+
[object]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# This is used by the resul transformer when the user executed a query where some groupings are repeated, so depth
|
18
|
+
# of the query doesn't match the length of the query result keys. We need to modify the result keys so everything
|
19
|
+
# matches again.
|
20
|
+
def duplicate(keys, values)
|
21
|
+
return if keys.length == values.length
|
22
|
+
|
23
|
+
duplicates = duplicates(keys)
|
24
|
+
return if duplicates.empty?
|
25
|
+
|
26
|
+
duplicates.each do |_, indices|
|
27
|
+
first_occurrence, *rest = indices
|
28
|
+
value_to_duplicate = values[first_occurrence]
|
29
|
+
rest.each { |index| values.insert(index, value_to_duplicate) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def duplicates(array)
|
36
|
+
map = {}
|
37
|
+
duplicates = {}
|
38
|
+
array.each_with_index do |v, i|
|
39
|
+
map[v] = (map[v] || 0) + 1
|
40
|
+
duplicates[v] ||= []
|
41
|
+
duplicates[v] << i
|
42
|
+
end
|
43
|
+
duplicates.select { |_, v| v.length > 1 }
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
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.
|
4
|
+
version: 0.2.0
|
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:
|
11
|
+
date: 2021-01-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '1.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: groupdate
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 5.2.1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 5.2.1
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: gruff
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -222,17 +236,20 @@ files:
|
|
222
236
|
- bin/setup
|
223
237
|
- graphql-groups.gemspec
|
224
238
|
- lib/graphql/groups.rb
|
225
|
-
- lib/graphql/groups/executor.rb
|
226
239
|
- lib/graphql/groups/group_type_registry.rb
|
227
240
|
- lib/graphql/groups/has_aggregates.rb
|
228
241
|
- lib/graphql/groups/has_groups.rb
|
229
|
-
- lib/graphql/groups/
|
242
|
+
- lib/graphql/groups/pending_query.rb
|
243
|
+
- lib/graphql/groups/query_builder.rb
|
244
|
+
- lib/graphql/groups/query_builder_context.rb
|
245
|
+
- lib/graphql/groups/query_result.rb
|
230
246
|
- lib/graphql/groups/result_transformer.rb
|
231
247
|
- lib/graphql/groups/schema/aggregate_field.rb
|
232
248
|
- lib/graphql/groups/schema/aggregate_type.rb
|
233
249
|
- lib/graphql/groups/schema/group_field.rb
|
234
250
|
- lib/graphql/groups/schema/group_result_type.rb
|
235
251
|
- lib/graphql/groups/schema/group_type.rb
|
252
|
+
- lib/graphql/groups/utils.rb
|
236
253
|
- lib/graphql/groups/version.rb
|
237
254
|
homepage: https://github.com/hschne/graphql-groups
|
238
255
|
licenses:
|
@@ -1,40 +0,0 @@
|
|
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
|
-
results = { key => results }
|
28
|
-
return results unless value[:nested]
|
29
|
-
|
30
|
-
value[:nested].each do |inner_key, inner_value|
|
31
|
-
new_key = (Array.wrap(key) << inner_key)
|
32
|
-
inner_result = execute(group_query, inner_key, inner_value)
|
33
|
-
results[new_key] = inner_result[inner_key]
|
34
|
-
end
|
35
|
-
results
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,54 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphQL
|
4
|
-
module Groups
|
5
|
-
class LookaheadParser
|
6
|
-
def self.parse(base_selection, context)
|
7
|
-
# TODO: Raise error if no aggregate selection is made
|
8
|
-
LookaheadParser.new(context).group_selections(base_selection, {})
|
9
|
-
end
|
10
|
-
|
11
|
-
def initialize(context)
|
12
|
-
@context = context
|
13
|
-
end
|
14
|
-
|
15
|
-
def group_selections(root, hash)
|
16
|
-
selections = root.selections
|
17
|
-
group_selections = selections.select { |selection| selection.field.is_a?(GraphQL::Groups::Schema::GroupField) }
|
18
|
-
group_selections.each do |selection|
|
19
|
-
own_query = get_field_proc(selection.field, selection.arguments)
|
20
|
-
hash[selection.name] ||= { proc: own_query }
|
21
|
-
hash[selection.name][:aggregates] = aggregates(selection)
|
22
|
-
end
|
23
|
-
group_selections
|
24
|
-
.filter { |selection| selection.selects?(:group_by) }
|
25
|
-
.each { |selection| hash[selection.name][:nested] = group_selections(selection.selection(:group_by), {}) }
|
26
|
-
hash
|
27
|
-
end
|
28
|
-
|
29
|
-
def get_field_proc(field, arguments)
|
30
|
-
proc { |**kwargs| field.owner.authorized_new(nil, @context).public_send(field.query_method, **arguments, **kwargs) }
|
31
|
-
end
|
32
|
-
|
33
|
-
def aggregates(group_selection)
|
34
|
-
aggregate_selections = group_selection.selections.select do |selection|
|
35
|
-
selection.field.is_a?(GraphQL::Groups::Schema::AggregateField)
|
36
|
-
end
|
37
|
-
aggregate_selections.each_with_object({}) do |selection, object|
|
38
|
-
name = selection.name
|
39
|
-
field = selection.field
|
40
|
-
if name == :count
|
41
|
-
proc = proc { |**kwargs| field.owner.send(:new, {}, nil).public_send(field.query_method, **kwargs) }
|
42
|
-
object[name] = { proc: proc }
|
43
|
-
elsif selection.field.own_attributes.present?
|
44
|
-
object[name] = { proc: get_aggregate_proc(field, selection.arguments), attributes: field.own_attributes }
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def get_aggregate_proc(field, arguments)
|
50
|
-
proc { |**kwargs| field.owner.authorized_new(nil, @context).send(field.query_method, **kwargs, **arguments) }
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|