activegraph-extensions 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +24 -0
- data/README.md +1 -0
- data/activegraph-extensions.gemspec +42 -0
- data/lib/active_graph_extensions/node/query/query_proxy.rb +16 -0
- data/lib/active_graph_extensions/node/query/query_proxy_eager_loading/association_eager_load.rb +32 -0
- data/lib/active_graph_extensions/node/query/query_proxy_eager_loading/association_limiting.rb +81 -0
- data/lib/active_graph_extensions/node/query/query_proxy_eager_loading/eager_loading_order.rb +42 -0
- data/lib/active_graph_extensions/node/query/query_proxy_eager_loading/enhanced_tree.rb +57 -0
- data/lib/active_graph_extensions/node/query/query_proxy_eager_loading/scope_eager_loading.rb +48 -0
- data/lib/active_graph_extensions/node/query/query_proxy_eager_loading.rb +149 -0
- data/lib/active_graph_extensions/string_parsers/relation_parser.rb +23 -0
- data/lib/active_graph_extensions/string_parsers/shared_rules.rb +21 -0
- data/lib/active_graph_extensions/version.rb +3 -0
- data/lib/active_graph_extensions.rb +12 -0
- metadata +259 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b28feac8e1598634606281d5dd8b70f019ac3038682abc635a5ef1d92c0e8b1e
|
4
|
+
data.tar.gz: 1c021a54e63267817077da6942f9fa4f0c61437e112de2677f54db7f440f653e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e6094af1c1c28ea6af47650e7b8399703e6c356addc72ac454b5c862c92fea563e284a5b80a96435cb41c9646a5f32f711cb0e314aea7a01c838d9476fe3d64b
|
7
|
+
data.tar.gz: 7e56b8b83defdf3d9b21887dbecad56eb21927ff5c72a742b4f477bc2ad422aa24ebc135ff054a8398e18ebae6fe83642cb31f9cd526adbce3b2ddd38ab2bddc
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Change Log
|
2
|
+
|
3
|
+
## [0.0.3] 2023-02-23
|
4
|
+
|
5
|
+
## Fixed
|
6
|
+
|
7
|
+
- Fixed issue where there was 500 error when we are eagerloding relations with multiple optional matches and some previous match was having authorization and next one was skipping authorization for those same nodes. This would result in a collection of related nodes where some nodes were null and corresponding next optional match nodes on path had values.
|
8
|
+
|
9
|
+
## [0.1.0] 2025-01-13
|
10
|
+
|
11
|
+
## Added
|
12
|
+
|
13
|
+
- Support for MRI
|
data/Gemfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
gem 'listen', '< 3.1'
|
6
|
+
active_model_version = ENV['ACTIVE_MODEL_VERSION']
|
7
|
+
gem 'activemodel', "~> #{active_model_version}" if active_model_version&.length&.positive?
|
8
|
+
|
9
|
+
#gem 'zeitwerk'
|
10
|
+
|
11
|
+
group :test do
|
12
|
+
gem 'coveralls', require: false
|
13
|
+
gem 'overcommit'
|
14
|
+
gem 'codecov', require: false
|
15
|
+
gem 'simplecov', require: false
|
16
|
+
gem 'simplecov-html', require: false
|
17
|
+
gem 'its'
|
18
|
+
gem 'test-unit'
|
19
|
+
gem 'colored'
|
20
|
+
gem 'dotenv'
|
21
|
+
gem 'timecop'
|
22
|
+
gem 'pry'
|
23
|
+
gem 'activegraph'
|
24
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Extensions to activegraph gem.
|
@@ -0,0 +1,42 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'active_graph_extensions/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'activegraph-extensions'
|
8
|
+
s.version = ActiveGraphExtensions::VERSION
|
9
|
+
|
10
|
+
s.required_ruby_version = '>= 2.5'
|
11
|
+
|
12
|
+
s.authors = 'Amit Suryavanshi'
|
13
|
+
s.email = 'amitbsuryavabshi@mail.com'
|
14
|
+
s.homepage = 'https://github.com/neo4jrb/activegraph-extensions/'
|
15
|
+
s.summary = 'Additional features to activegraph'
|
16
|
+
s.license = 'MIT'
|
17
|
+
s.description = <<-DESCRIPTION
|
18
|
+
Additional features to activegraph, like sideload limiting, authorizing sideloads etc.
|
19
|
+
DESCRIPTION
|
20
|
+
|
21
|
+
s.require_path = 'lib'
|
22
|
+
s.files = Dir.glob('{bin,lib,config}/**/*') + %w(README.md CHANGELOG.md Gemfile activegraph-extensions.gemspec)
|
23
|
+
s.executables = []
|
24
|
+
s.extra_rdoc_files = %w( README.md )
|
25
|
+
s.rdoc_options = ['--quiet', '--title', 'Neo4j.rb', '--line-numbers', '--main', 'README.rdoc', '--inline-source']
|
26
|
+
|
27
|
+
s.add_dependency('parslet')
|
28
|
+
s.add_dependency('activegraph', '>= 12.0.0.beta.5')
|
29
|
+
|
30
|
+
s.add_development_dependency('guard')
|
31
|
+
s.add_development_dependency('guard-rspec')
|
32
|
+
s.add_development_dependency('guard-rubocop')
|
33
|
+
s.add_development_dependency('neo4j-rake_tasks', '>= 0.3.0')
|
34
|
+
s.add_development_dependency('os')
|
35
|
+
s.add_development_dependency('pry')
|
36
|
+
s.add_development_dependency('railties', '>= 4.0')
|
37
|
+
s.add_development_dependency('rake')
|
38
|
+
s.add_development_dependency('rubocop', '>= 0.56.0')
|
39
|
+
s.add_development_dependency('yard')
|
40
|
+
s.add_development_dependency('dryspec')
|
41
|
+
s.add_development_dependency('rspec', '< 3.10') # Cannot proxy frozen objects
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphExtensions
|
4
|
+
module Node
|
5
|
+
module Query
|
6
|
+
# We need earlier proxy to generate cypher in query proxy eagerloading
|
7
|
+
module QueryProxy
|
8
|
+
def branch(&block)
|
9
|
+
proxy = super
|
10
|
+
proxy.instance_variable_set(:@break_proxy, as(identity).instance_eval(&block))
|
11
|
+
proxy
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/active_graph_extensions/node/query/query_proxy_eager_loading/association_eager_load.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphExtensions
|
4
|
+
module Node
|
5
|
+
module Query
|
6
|
+
module QueryProxyEagerLoading
|
7
|
+
# Used for eager loading associations with scope
|
8
|
+
module AssociationEagerLoad
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
def associations_to_eagerload
|
13
|
+
@associations_to_eagerload
|
14
|
+
end
|
15
|
+
|
16
|
+
def association_nodes(key, ids, filter)
|
17
|
+
send(@associations_to_eagerload[key], ids, filter)
|
18
|
+
end
|
19
|
+
|
20
|
+
def eagerload_associations(config)
|
21
|
+
@associations_to_eagerload = config
|
22
|
+
end
|
23
|
+
|
24
|
+
def eagerload_association?(key)
|
25
|
+
@associations_to_eagerload.keys.include?(key)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphExtensions
|
4
|
+
module Node
|
5
|
+
module Query
|
6
|
+
module QueryProxyEagerLoading
|
7
|
+
module AssociationLimiting
|
8
|
+
def self.included(base)
|
9
|
+
base.attr_reader(:max_page_size)
|
10
|
+
base.attr_reader(:paginate)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def rel_collection_str(path)
|
16
|
+
collection_name = "[#{relationship_name(path)}, #{escape(path_name(path))}] "
|
17
|
+
collection = apply_limit?(path) ? "apoc.agg.slice(#{collection_name}, 0, #{association_limit(path)})" : "collect(#{collection_name})"
|
18
|
+
"#{collection} AS #{escape("#{path_name(path)}_collection")}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def apply_limit?(path)
|
22
|
+
!multipath?(path) && (path.last&.association_limit || paginate)
|
23
|
+
end
|
24
|
+
|
25
|
+
def relationship_name(path)
|
26
|
+
if path.last.rel_length
|
27
|
+
"last(relationships(#{escape("#{path_name(path)}_path")}))"
|
28
|
+
else
|
29
|
+
escape("#{path_name(path)}_rel")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def convert_to_list(collection_name, limit)
|
34
|
+
limit.present? ? "apoc.agg.slice(#{collection_name}, 0, #{limit})" : "collect(#{collection_name})"
|
35
|
+
end
|
36
|
+
|
37
|
+
def association_limit(path)
|
38
|
+
limit = path.last&.association_limit
|
39
|
+
limit.blank? || limit.to_i > max_page_size ? max_page_size : limit
|
40
|
+
end
|
41
|
+
|
42
|
+
def with_association_query_part(base_query, path, previous_with_vars)
|
43
|
+
with_args = [identity, rel_collection_str(path), *previous_with_vars]
|
44
|
+
|
45
|
+
optional_match_with_where(base_query, path, previous_with_vars).with(with_args)
|
46
|
+
end
|
47
|
+
|
48
|
+
def limit_node_in_where_clause(query, path)
|
49
|
+
(path.length - 1).times.inject(query) do |query_with_where, index|
|
50
|
+
query_with_where.where("`#{path_name(path[0..index])}` in [i IN #{node_from_collection(path[0..index])} | i[1]]")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def node_from_collection(path_step)
|
55
|
+
"`#{path_name(path_step)}_collection`"
|
56
|
+
end
|
57
|
+
|
58
|
+
def path_alias(node_alias)
|
59
|
+
var_fix(node_alias, :path)
|
60
|
+
end
|
61
|
+
|
62
|
+
def rel_alias(node_alias)
|
63
|
+
var_fix(node_alias, :rel)
|
64
|
+
end
|
65
|
+
|
66
|
+
def multipath?(path)
|
67
|
+
path.size > 1
|
68
|
+
end
|
69
|
+
|
70
|
+
def association_limit_present?(path)
|
71
|
+
association_limit(path).present?
|
72
|
+
end
|
73
|
+
|
74
|
+
def multipath_with_sideload_limit?(path)
|
75
|
+
multipath?(path) && association_limit_present?(path[0..0])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphExtensions
|
4
|
+
module Node
|
5
|
+
module Query
|
6
|
+
module QueryProxyEagerLoading
|
7
|
+
# Used to append auth scopes to query proxy eagerloading
|
8
|
+
module EagerLoadingOrder
|
9
|
+
def optional_order(query, path, previous_with_vars)
|
10
|
+
node_alias = path_name(path)
|
11
|
+
order_clause = order_clause_for_query(node_alias)
|
12
|
+
if path.last.rel_length
|
13
|
+
order_clause.reject! { |el| el.include?('_rel') }
|
14
|
+
query.order("length(`#{node_alias}_path`)", *order_clause)
|
15
|
+
.with(*with_variables(path, node_alias, previous_with_vars))
|
16
|
+
else
|
17
|
+
query.order(*order_clause).with(*with_variables(path, node_alias, previous_with_vars))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def order_clause_for_query(node_alias)
|
22
|
+
(order = @order_spec&.fetch(node_alias, nil)) ? order.map(&method(:nested_order_clause).curry.call(node_alias)) : []
|
23
|
+
end
|
24
|
+
|
25
|
+
def nested_order_clause(node_alias, order_spec)
|
26
|
+
[node_or_rel_alias(node_alias, order_spec), name(order_spec)].join('.')
|
27
|
+
end
|
28
|
+
|
29
|
+
def order_clause(key, order_spec)
|
30
|
+
property_with_direction = name(order_spec)
|
31
|
+
node_alias = node_aliase_for_collection(key, order_spec) || node_aliase_for_order(property_with_direction)
|
32
|
+
[node_alias, property_with_direction].compact.join('.')
|
33
|
+
end
|
34
|
+
|
35
|
+
def skip_order?
|
36
|
+
@order_spec.blank? || @order_spec.keys.all?(&:blank?)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphExtensions
|
4
|
+
module Node
|
5
|
+
module Query
|
6
|
+
module QueryProxyEagerLoading
|
7
|
+
# Tree allowing storage of additional information about the associations
|
8
|
+
class EnhancedTree < ::ActiveGraph::Node::Query::QueryProxyEagerLoading::AssociationTree
|
9
|
+
attr_reader :options, :association_limit
|
10
|
+
|
11
|
+
def initialize(model, name = nil, rel_length = nil, association_limit = nil)
|
12
|
+
@association_limit = association_limit
|
13
|
+
super(model, name, rel_length)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_spec_and_validate(spec)
|
17
|
+
add_spec(spec)
|
18
|
+
validate_for_zero_length_paths
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate_for_zero_length_paths
|
22
|
+
fail 'Can not eager load more than one zero length path.' if values.count(&:zero_length_path?) > 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def zero_length_path?
|
26
|
+
rel_length&.fetch(:min, nil)&.to_s == '0' || values.any?(&:zero_length_path?)
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_key(key, length = nil, assoc_limit = nil)
|
30
|
+
self[key] ||= self.class.new(model, key, length, assoc_limit)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_nested(key, value, length = nil, assoc_limit = nil)
|
34
|
+
add_key(key, length, assoc_limit).add_spec(value)
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_string(str)
|
38
|
+
map = StringParsers::RelationParser.new.parse(str)
|
39
|
+
add_nested(map[:rel_name].to_sym, map[:rest_str].to_s.presence, map[:length_part], map[:limit_digit])
|
40
|
+
end
|
41
|
+
|
42
|
+
def process_hash(spec)
|
43
|
+
spec = spec.dup
|
44
|
+
@options = spec.delete(:_options)
|
45
|
+
super(spec)
|
46
|
+
end
|
47
|
+
|
48
|
+
def target_class(model, key)
|
49
|
+
association = model.associations[key.to_sym]
|
50
|
+
fail "Invalid association: #{[*path, key].join('.')}" unless association
|
51
|
+
model.associations[key].target_class
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphExtensions
|
4
|
+
module Node
|
5
|
+
module Query
|
6
|
+
module QueryProxyEagerLoading
|
7
|
+
# Used to append auth scopes to query proxy eagerloading
|
8
|
+
module ScopeEagerLoading
|
9
|
+
def authorized_rel(path, var)
|
10
|
+
rel_model = relationship_model(path)
|
11
|
+
return {} if @opts.blank? || !(auth_scope = authorized_scope(rel_model, path))
|
12
|
+
conf = { rels: [], chain: {} }
|
13
|
+
proxy = auth_scope.call(var, "#{var}_rel", @opts.merge(properties: properties_for(rel_model),
|
14
|
+
rel_length: path.last.rel_length))
|
15
|
+
proxy_rel_parts(proxy.instance_variable_get(:@break_proxy) || proxy, conf)
|
16
|
+
conf
|
17
|
+
end
|
18
|
+
|
19
|
+
def properties_for(rel_model)
|
20
|
+
return [] unless @opts[:properties]
|
21
|
+
@opts[:properties].select { |prop| prop.model.name == rel_model.name }.map(&:name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def relationship_model(path)
|
25
|
+
path[0..-2].inject(model) { |mod, rel| mod.send(rel.name).model }
|
26
|
+
end
|
27
|
+
|
28
|
+
def authorized_scope(rel_model, path)
|
29
|
+
rel_model.scopes["authorized_#{path.last.association.name}".to_sym]
|
30
|
+
end
|
31
|
+
|
32
|
+
def proxy_rel_parts(auth_proxy, conf)
|
33
|
+
return unless auth_proxy&.association
|
34
|
+
rel_length = auth_proxy.instance_variable_get(:@rel_length)
|
35
|
+
conf[:rels] << relationship_part(auth_proxy.association, auth_proxy.identity, rel_length)
|
36
|
+
assign_config_chain(conf, auth_proxy, rel_length)
|
37
|
+
proxy_rel_parts(auth_proxy.query_proxy, conf)
|
38
|
+
end
|
39
|
+
|
40
|
+
def assign_config_chain(conf, auth_proxy, rel_length)
|
41
|
+
return unless (auth_chain = auth_proxy.instance_variable_get(:@chain))
|
42
|
+
conf[:chain][[auth_proxy.identity, rel_length ? "#{auth_proxy.identity}_rel" : nil]] = auth_chain
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module ActiveGraphExtensions
|
2
|
+
module Node
|
3
|
+
module Query
|
4
|
+
module QueryProxyEagerLoading
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include AssociationLimiting
|
7
|
+
include ScopeEagerLoading
|
8
|
+
include EagerLoadingOrder
|
9
|
+
|
10
|
+
def association_tree_class
|
11
|
+
EnhancedTree
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_ordered_associations(spec, order, opts = {})
|
15
|
+
@max_page_size = opts[:max_page_size]
|
16
|
+
@paginate = opts[:paginate]
|
17
|
+
@with_vars = opts[:with_vars]
|
18
|
+
@order_spec = order.with_indifferent_access unless spec.empty?
|
19
|
+
@opts = opts
|
20
|
+
with_associations(spec)
|
21
|
+
end
|
22
|
+
|
23
|
+
def first
|
24
|
+
limit(1).to_a.first
|
25
|
+
end
|
26
|
+
|
27
|
+
class_methods do
|
28
|
+
def rel?(order_spec)
|
29
|
+
order_spec.is_a?(Hash) ? 0 : 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def with_associations(*spec)
|
34
|
+
new_link.tap do |new_query_proxy|
|
35
|
+
new_query_proxy.with_associations_tree = with_associations_tree.clone
|
36
|
+
new_query_proxy.with_associations_tree.add_spec_and_validate(spec)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def query_from_association_tree
|
43
|
+
previous_with_vars = defalut_previous_with_vars
|
44
|
+
with_associations_tree.paths.inject(query_as(identity).with(base_query_with_vars)) do |query, path|
|
45
|
+
with_association_query_part(query, path, previous_with_vars).tap do
|
46
|
+
previous_with_vars << var_fix(path_name(path), :collection)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def defalut_previous_with_vars
|
52
|
+
@with_vars&.dup || []
|
53
|
+
end
|
54
|
+
|
55
|
+
def base_query_with_vars
|
56
|
+
[ensure_distinct(identity)] + (@with_vars || [])
|
57
|
+
end
|
58
|
+
|
59
|
+
def optional_match_with_where(query, path, vars)
|
60
|
+
computed_query = super
|
61
|
+
computed_query = limit_node_in_where_clause(computed_query, path) if multipath?(path)
|
62
|
+
skip_order? && !path.last.rel_length ? computed_query : optional_order(computed_query, path, vars)
|
63
|
+
end
|
64
|
+
|
65
|
+
def optional_match(base_query, path)
|
66
|
+
start_path = "#{escape("#{path_name(path)}_path")}=(#{identity})"
|
67
|
+
conf = authorized_rel(path, path_name(path[0..-1]))
|
68
|
+
query = construct_optional_match(start_path, base_query, conf[:rels] ? path[0..-2] : path, conf[:rels])
|
69
|
+
conf[:rels] ? apply_chain(conf[:chain], query) : query
|
70
|
+
end
|
71
|
+
|
72
|
+
def construct_optional_match(start_path, base_query, path, scope_rels)
|
73
|
+
base_query.optional_match(
|
74
|
+
"#{start_path}#{path.each_with_index.map do |element, index|
|
75
|
+
relationship_part(element.association, path_name(path[0..index]), element.rel_length)
|
76
|
+
end.join}#{(scope_rels || []).reverse.join}"
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def apply_chain(chain, query)
|
81
|
+
chain.each do |key, links|
|
82
|
+
query = links.inject(query) do |q, link|
|
83
|
+
args = link.args(*key)
|
84
|
+
args.is_a?(Array) ? q.send(link.clause, *args) : q.send(link.clause, args)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
query
|
88
|
+
end
|
89
|
+
|
90
|
+
def with_variables(path, node_alias, previous_with_vars)
|
91
|
+
[identity, path.last.rel_length ? path_alias(node_alias) : rel_alias(node_alias), var_fix(node_alias)] +
|
92
|
+
previous_with_vars
|
93
|
+
end
|
94
|
+
|
95
|
+
def before_pluck(query)
|
96
|
+
return query if skip_order? && !include_with_path_length?
|
97
|
+
base_query = query.order(
|
98
|
+
(@order_spec || []).flat_map { |key, order_specs| order_specs.map(&method(:order_clause).curry.call(key)) }
|
99
|
+
)
|
100
|
+
query_from_chain(@postponed_chain, base_query, identity)
|
101
|
+
end
|
102
|
+
|
103
|
+
def node_aliase_for_collection(key, order_spec)
|
104
|
+
"#{var(key, :collection, &:itself)}[0][#{self.class.rel?(order_spec)}]" if key.present?
|
105
|
+
end
|
106
|
+
|
107
|
+
def node_aliase_for_order(property_with_direction)
|
108
|
+
identity unless @with_vars&.include?(property_with_direction.split(' ').first.to_sym)
|
109
|
+
end
|
110
|
+
|
111
|
+
def name(order_spec)
|
112
|
+
Array(order_spec).flatten.last.to_s
|
113
|
+
end
|
114
|
+
|
115
|
+
def node_or_rel_alias(node_alias, order_spec)
|
116
|
+
var(node_alias, order_spec.is_a?(Hash) ? :rel : nil, &:itself)
|
117
|
+
end
|
118
|
+
|
119
|
+
CLAUSES_TO_POSTPONE = %i[limit order skip].freeze
|
120
|
+
|
121
|
+
def include_with_path_length?(path = @with_associations_tree)
|
122
|
+
path.present? && (path.rel_length.present? || path.any? { |_, val| include_with_path_length?(val) })
|
123
|
+
end
|
124
|
+
|
125
|
+
def chain
|
126
|
+
return super if skip_order? && !include_with_path_length?
|
127
|
+
clauses = !skip_order? ? CLAUSES_TO_POSTPONE : %i[order]
|
128
|
+
@postponed_chain, other_chain = super.partition { |link| clauses.include?(link.clause) }
|
129
|
+
other_chain
|
130
|
+
end
|
131
|
+
|
132
|
+
def perform_query
|
133
|
+
@_cache = ActiveGraph::Node::Query::QueryProxyEagerLoading::IdentityMap.new
|
134
|
+
build_query
|
135
|
+
.map do |record, eager_data|
|
136
|
+
record = cache_and_init(record, with_associations_tree)
|
137
|
+
eager_data.zip(with_associations_tree.paths.map(&:last)).each do |eager_records, element|
|
138
|
+
eager_records.each do |eager_record|
|
139
|
+
next unless eager_record.first&.type&.to_s == element.association.relationship_type.to_s
|
140
|
+
add_to_cache(*eager_record, element)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
record
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphExtensions
|
4
|
+
module StringParsers
|
5
|
+
# Parsing relationships with length
|
6
|
+
class RelationParser < ::Parslet::Parser
|
7
|
+
include SharedRules
|
8
|
+
|
9
|
+
# TODO: It is very bad to build a grammar with none terminals. Please note that none here are necessary to
|
10
|
+
# mimic the previous behavior of `repeat` which is `repeat(0)` which effectively allows empty strings as
|
11
|
+
# identifiers
|
12
|
+
rule(:zero) { str('0') }
|
13
|
+
rule(:length_1) { zero.as(:min) >> range >> number?.maybe.as(:max) }
|
14
|
+
rule(:length_2) { number?.maybe.as(:max) }
|
15
|
+
rule(:length) { asterisk >> (length_1 | length_2) }
|
16
|
+
rule(:limit) { number?.as(:limit_digit) >> asterisk }
|
17
|
+
rule(:key) { limit.maybe >> rel >> length.as(:length_part).maybe }
|
18
|
+
rule(:anything) { match('.').repeat }
|
19
|
+
rule(:root) { key >> dot.maybe >> anything.maybe.as(:rest_str) }
|
20
|
+
rule(:rel_sequence) { infix_expression(key, [dot, 1, :left]) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphExtensions
|
4
|
+
module StringParsers
|
5
|
+
module SharedRules
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
VAR_CHAR = 'a-z_'
|
8
|
+
included do
|
9
|
+
rule(:asterisk) { str('*') }
|
10
|
+
rule(:number) { match('[\d]').repeat(1) }
|
11
|
+
rule(:none) { str('') }
|
12
|
+
rule(:number?) { number | none }
|
13
|
+
rule(:range) { str('..') }
|
14
|
+
rule(:dot) { str('.') }
|
15
|
+
rule(:identifier) { match("[#{VAR_CHAR}]") >> match("[#{VAR_CHAR}0-9]").repeat(0) }
|
16
|
+
rule(:identifier?) { identifier | none }
|
17
|
+
rule(:rel) { identifier?.as(:rel_name) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'active_graph'
|
2
|
+
require 'parslet'
|
3
|
+
|
4
|
+
module ActiveGraphExtensions
|
5
|
+
end
|
6
|
+
|
7
|
+
Zeitwerk::Loader.for_gem.setup
|
8
|
+
|
9
|
+
ActiveGraph::Node::Query::QueryProxy.include ActiveGraphExtensions::Node::Query::QueryProxyEagerLoading
|
10
|
+
ActiveGraph::Node::Query::QueryProxy.prepend ActiveGraphExtensions::Node::Query::QueryProxy
|
11
|
+
|
12
|
+
ActiveGraph::Node.include ActiveGraphExtensions::Node::Query::QueryProxyEagerLoading::AssociationEagerLoad
|
metadata
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activegraph-extensions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Amit Suryavanshi
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-01-13 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: parslet
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activegraph
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 12.0.0.beta.5
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 12.0.0.beta.5
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: guard
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: guard-rspec
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: guard-rubocop
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: neo4j-rake_tasks
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 0.3.0
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 0.3.0
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: os
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: pry
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: railties
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '4.0'
|
131
|
+
type: :development
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '4.0'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: rake
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
type: :development
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
- !ruby/object:Gem::Dependency
|
153
|
+
name: rubocop
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: 0.56.0
|
159
|
+
type: :development
|
160
|
+
prerelease: false
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: 0.56.0
|
166
|
+
- !ruby/object:Gem::Dependency
|
167
|
+
name: yard
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
type: :development
|
174
|
+
prerelease: false
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
- !ruby/object:Gem::Dependency
|
181
|
+
name: dryspec
|
182
|
+
requirement: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
type: :development
|
188
|
+
prerelease: false
|
189
|
+
version_requirements: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - ">="
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0'
|
194
|
+
- !ruby/object:Gem::Dependency
|
195
|
+
name: rspec
|
196
|
+
requirement: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - "<"
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '3.10'
|
201
|
+
type: :development
|
202
|
+
prerelease: false
|
203
|
+
version_requirements: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - "<"
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: '3.10'
|
208
|
+
description: " Additional features to activegraph, like sideload limiting, authorizing
|
209
|
+
sideloads etc.\n"
|
210
|
+
email: amitbsuryavabshi@mail.com
|
211
|
+
executables: []
|
212
|
+
extensions: []
|
213
|
+
extra_rdoc_files:
|
214
|
+
- README.md
|
215
|
+
files:
|
216
|
+
- CHANGELOG.md
|
217
|
+
- Gemfile
|
218
|
+
- README.md
|
219
|
+
- activegraph-extensions.gemspec
|
220
|
+
- lib/active_graph_extensions.rb
|
221
|
+
- lib/active_graph_extensions/node/query/query_proxy.rb
|
222
|
+
- lib/active_graph_extensions/node/query/query_proxy_eager_loading.rb
|
223
|
+
- lib/active_graph_extensions/node/query/query_proxy_eager_loading/association_eager_load.rb
|
224
|
+
- lib/active_graph_extensions/node/query/query_proxy_eager_loading/association_limiting.rb
|
225
|
+
- lib/active_graph_extensions/node/query/query_proxy_eager_loading/eager_loading_order.rb
|
226
|
+
- lib/active_graph_extensions/node/query/query_proxy_eager_loading/enhanced_tree.rb
|
227
|
+
- lib/active_graph_extensions/node/query/query_proxy_eager_loading/scope_eager_loading.rb
|
228
|
+
- lib/active_graph_extensions/string_parsers/relation_parser.rb
|
229
|
+
- lib/active_graph_extensions/string_parsers/shared_rules.rb
|
230
|
+
- lib/active_graph_extensions/version.rb
|
231
|
+
homepage: https://github.com/neo4jrb/activegraph-extensions/
|
232
|
+
licenses:
|
233
|
+
- MIT
|
234
|
+
metadata: {}
|
235
|
+
rdoc_options:
|
236
|
+
- "--quiet"
|
237
|
+
- "--title"
|
238
|
+
- Neo4j.rb
|
239
|
+
- "--line-numbers"
|
240
|
+
- "--main"
|
241
|
+
- README.rdoc
|
242
|
+
- "--inline-source"
|
243
|
+
require_paths:
|
244
|
+
- lib
|
245
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
246
|
+
requirements:
|
247
|
+
- - ">="
|
248
|
+
- !ruby/object:Gem::Version
|
249
|
+
version: '2.5'
|
250
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
251
|
+
requirements:
|
252
|
+
- - ">="
|
253
|
+
- !ruby/object:Gem::Version
|
254
|
+
version: '0'
|
255
|
+
requirements: []
|
256
|
+
rubygems_version: 3.6.2
|
257
|
+
specification_version: 4
|
258
|
+
summary: Additional features to activegraph
|
259
|
+
test_files: []
|