graphql-groups 0.1.4 → 0.2.0
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 +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
|