neo4j_legacy 7.2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1357 -0
- data/CONTRIBUTORS +8 -0
- data/Gemfile +38 -0
- data/README.md +103 -0
- data/bin/neo4j-jars +33 -0
- data/bin/rake +17 -0
- data/config/locales/en.yml +5 -0
- data/config/neo4j/add_classnames.yml +1 -0
- data/config/neo4j/config.yml +35 -0
- data/lib/active_support/per_thread_registry.rb +1 -0
- data/lib/backports/action_controller/metal/strong_parameters.rb +672 -0
- data/lib/backports/active_model/forbidden_attributes_protection.rb +30 -0
- data/lib/backports/active_support/concern.rb +13 -0
- data/lib/backports/active_support/core_ext/module/attribute_accessors.rb +10 -0
- data/lib/backports/active_support/logger.rb +99 -0
- data/lib/backports/active_support/logger_silence.rb +27 -0
- data/lib/backports/active_support/logger_thread_safe_level.rb +32 -0
- data/lib/backports/active_support/per_thread_registry.rb +60 -0
- data/lib/backports.rb +4 -0
- data/lib/neo4j/active_node/callbacks.rb +8 -0
- data/lib/neo4j/active_node/dependent/association_methods.rb +48 -0
- data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +50 -0
- data/lib/neo4j/active_node/dependent.rb +11 -0
- data/lib/neo4j/active_node/enum.rb +29 -0
- data/lib/neo4j/active_node/has_n/association/rel_factory.rb +61 -0
- data/lib/neo4j/active_node/has_n/association/rel_wrapper.rb +23 -0
- data/lib/neo4j/active_node/has_n/association.rb +280 -0
- data/lib/neo4j/active_node/has_n/association_cypher_methods.rb +108 -0
- data/lib/neo4j/active_node/has_n.rb +532 -0
- data/lib/neo4j/active_node/id_property/accessor.rb +62 -0
- data/lib/neo4j/active_node/id_property.rb +187 -0
- data/lib/neo4j/active_node/initialize.rb +21 -0
- data/lib/neo4j/active_node/labels/index.rb +87 -0
- data/lib/neo4j/active_node/labels/reloading.rb +21 -0
- data/lib/neo4j/active_node/labels.rb +198 -0
- data/lib/neo4j/active_node/node_wrapper.rb +52 -0
- data/lib/neo4j/active_node/orm_adapter.rb +82 -0
- data/lib/neo4j/active_node/persistence.rb +175 -0
- data/lib/neo4j/active_node/property.rb +60 -0
- data/lib/neo4j/active_node/query/query_proxy.rb +361 -0
- data/lib/neo4j/active_node/query/query_proxy_eager_loading.rb +61 -0
- data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +90 -0
- data/lib/neo4j/active_node/query/query_proxy_find_in_batches.rb +19 -0
- data/lib/neo4j/active_node/query/query_proxy_link.rb +117 -0
- data/lib/neo4j/active_node/query/query_proxy_methods.rb +210 -0
- data/lib/neo4j/active_node/query/query_proxy_methods_of_mass_updating.rb +83 -0
- data/lib/neo4j/active_node/query.rb +76 -0
- data/lib/neo4j/active_node/query_methods.rb +65 -0
- data/lib/neo4j/active_node/reflection.rb +86 -0
- data/lib/neo4j/active_node/rels.rb +11 -0
- data/lib/neo4j/active_node/scope.rb +146 -0
- data/lib/neo4j/active_node/unpersisted.rb +48 -0
- data/lib/neo4j/active_node/validations.rb +59 -0
- data/lib/neo4j/active_node.rb +105 -0
- data/lib/neo4j/active_rel/callbacks.rb +15 -0
- data/lib/neo4j/active_rel/initialize.rb +28 -0
- data/lib/neo4j/active_rel/persistence/query_factory.rb +95 -0
- data/lib/neo4j/active_rel/persistence.rb +114 -0
- data/lib/neo4j/active_rel/property.rb +95 -0
- data/lib/neo4j/active_rel/query.rb +95 -0
- data/lib/neo4j/active_rel/rel_wrapper.rb +22 -0
- data/lib/neo4j/active_rel/related_node.rb +83 -0
- data/lib/neo4j/active_rel/types.rb +82 -0
- data/lib/neo4j/active_rel/validations.rb +8 -0
- data/lib/neo4j/active_rel.rb +67 -0
- data/lib/neo4j/class_arguments.rb +39 -0
- data/lib/neo4j/config.rb +124 -0
- data/lib/neo4j/core/query.rb +22 -0
- data/lib/neo4j/errors.rb +28 -0
- data/lib/neo4j/migration.rb +127 -0
- data/lib/neo4j/paginated.rb +27 -0
- data/lib/neo4j/railtie.rb +169 -0
- data/lib/neo4j/schema/operation.rb +91 -0
- data/lib/neo4j/shared/attributes.rb +220 -0
- data/lib/neo4j/shared/callbacks.rb +64 -0
- data/lib/neo4j/shared/cypher.rb +37 -0
- data/lib/neo4j/shared/declared_properties.rb +204 -0
- data/lib/neo4j/shared/declared_property/index.rb +37 -0
- data/lib/neo4j/shared/declared_property.rb +118 -0
- data/lib/neo4j/shared/enum.rb +148 -0
- data/lib/neo4j/shared/filtered_hash.rb +79 -0
- data/lib/neo4j/shared/identity.rb +28 -0
- data/lib/neo4j/shared/initialize.rb +28 -0
- data/lib/neo4j/shared/marshal.rb +23 -0
- data/lib/neo4j/shared/mass_assignment.rb +58 -0
- data/lib/neo4j/shared/permitted_attributes.rb +28 -0
- data/lib/neo4j/shared/persistence.rb +231 -0
- data/lib/neo4j/shared/property.rb +220 -0
- data/lib/neo4j/shared/query_factory.rb +101 -0
- data/lib/neo4j/shared/rel_type_converters.rb +43 -0
- data/lib/neo4j/shared/serialized_properties.rb +30 -0
- data/lib/neo4j/shared/type_converters.rb +418 -0
- data/lib/neo4j/shared/typecasted_attributes.rb +98 -0
- data/lib/neo4j/shared/typecaster.rb +53 -0
- data/lib/neo4j/shared/validations.rb +48 -0
- data/lib/neo4j/shared.rb +51 -0
- data/lib/neo4j/tasks/migration.rake +24 -0
- data/lib/neo4j/timestamps/created.rb +9 -0
- data/lib/neo4j/timestamps/updated.rb +9 -0
- data/lib/neo4j/timestamps.rb +11 -0
- data/lib/neo4j/type_converters.rb +7 -0
- data/lib/neo4j/version.rb +3 -0
- data/lib/neo4j/wrapper.rb +4 -0
- data/lib/neo4j.rb +96 -0
- data/lib/rails/generators/neo4j/model/model_generator.rb +86 -0
- data/lib/rails/generators/neo4j/model/templates/model.erb +15 -0
- data/lib/rails/generators/neo4j_generator.rb +67 -0
- data/neo4j.gemspec +43 -0
- metadata +389 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module ActiveNode
|
3
|
+
module Query
|
4
|
+
# Methods related to returning nodes and rels from QueryProxy
|
5
|
+
module QueryProxyEnumerable
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# Just like every other <tt>each</tt> but it allows for optional params to support the versions that also return relationships.
|
9
|
+
# The <tt>node</tt> and <tt>rel</tt> params are typically used by those other methods but there's nothing stopping you from
|
10
|
+
# using `your_node.each(true, true)` instead of `your_node.each_with_rel`.
|
11
|
+
# @return [Enumerable] An enumerable containing some combination of nodes and rels.
|
12
|
+
def each(node = true, rel = nil, &block)
|
13
|
+
result(node, rel).each(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def result(node = true, rel = nil)
|
17
|
+
@result_cache ||= {}
|
18
|
+
return result_cache_for(node, rel) if result_cache?(node, rel)
|
19
|
+
|
20
|
+
pluck_vars = []
|
21
|
+
pluck_vars << identity if node
|
22
|
+
pluck_vars << @rel_var if rel
|
23
|
+
|
24
|
+
result = pluck(*pluck_vars)
|
25
|
+
|
26
|
+
result.each do |object|
|
27
|
+
object.instance_variable_set('@source_query_proxy', self)
|
28
|
+
object.instance_variable_set('@source_proxy_result_cache', result)
|
29
|
+
end
|
30
|
+
|
31
|
+
@result_cache[[node, rel]] ||= result
|
32
|
+
end
|
33
|
+
|
34
|
+
def result_cache?(node = true, rel = nil)
|
35
|
+
!!result_cache_for(node, rel)
|
36
|
+
end
|
37
|
+
|
38
|
+
def result_cache_for(node = true, rel = nil)
|
39
|
+
(@result_cache || {})[[node, rel]]
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch_result_cache
|
43
|
+
@result_cache ||= yield
|
44
|
+
end
|
45
|
+
|
46
|
+
# When called at the end of a QueryProxy chain, it will return the resultant relationship objects intead of nodes.
|
47
|
+
# For example, to return the relationship between a given student and their lessons:
|
48
|
+
#
|
49
|
+
# .. code-block:: ruby
|
50
|
+
#
|
51
|
+
# student.lessons.each_rel do |rel|
|
52
|
+
#
|
53
|
+
# @return [Enumerable] An enumerable containing any number of applicable relationship objects.
|
54
|
+
def each_rel(&block)
|
55
|
+
block_given? ? each(false, true, &block) : to_enum(:each, false, true)
|
56
|
+
end
|
57
|
+
|
58
|
+
# When called at the end of a QueryProxy chain, it will return the nodes and relationships of the last link.
|
59
|
+
# For example, to return a lesson and each relationship to a given student:
|
60
|
+
#
|
61
|
+
# .. code-block:: ruby
|
62
|
+
#
|
63
|
+
# student.lessons.each_with_rel do |lesson, rel|
|
64
|
+
def each_with_rel(&block)
|
65
|
+
block_given? ? each(true, true, &block) : to_enum(:each, true, true)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Does exactly what you would hope. Without it, comparing `bobby.lessons == sandy.lessons` would evaluate to false because it
|
69
|
+
# would be comparing the QueryProxy objects, not the lessons themselves.
|
70
|
+
def ==(other)
|
71
|
+
self.to_a == other
|
72
|
+
end
|
73
|
+
|
74
|
+
# For getting variables which have been defined as part of the association chain
|
75
|
+
def pluck(*args)
|
76
|
+
transformable_attributes = (model ? model.attribute_names : []) + %w(uuid neo_id)
|
77
|
+
arg_list = args.map do |arg|
|
78
|
+
if transformable_attributes.include?(arg.to_s)
|
79
|
+
{identity => arg}
|
80
|
+
else
|
81
|
+
arg
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
self.query.pluck(*arg_list)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module ActiveNode
|
3
|
+
module Query
|
4
|
+
module QueryProxyFindInBatches
|
5
|
+
def find_in_batches(options = {})
|
6
|
+
query.return(identity).find_in_batches(identity, @model.primary_key, options) do |batch|
|
7
|
+
yield batch.map(&identity)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_each(options = {})
|
12
|
+
query.return(identity).find_each(identity, @model.primary_key, options) do |result|
|
13
|
+
yield result.send(identity)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module ActiveNode
|
3
|
+
module Query
|
4
|
+
class QueryProxy
|
5
|
+
class Link
|
6
|
+
attr_reader :clause
|
7
|
+
|
8
|
+
def initialize(clause, arg, args = [])
|
9
|
+
@clause = clause
|
10
|
+
@arg = arg
|
11
|
+
@args = args
|
12
|
+
end
|
13
|
+
|
14
|
+
def args(var, rel_var)
|
15
|
+
@arg.respond_to?(:call) ? @arg.call(var, rel_var) : [@arg, @args].flatten
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def for_clause(clause, arg, model, *args)
|
20
|
+
method_to_call = "for_#{clause}_clause"
|
21
|
+
|
22
|
+
send(method_to_call, arg, model, *args)
|
23
|
+
end
|
24
|
+
|
25
|
+
def for_where_clause(arg, model, *args)
|
26
|
+
node_num = 1
|
27
|
+
result = []
|
28
|
+
if arg.is_a?(Hash)
|
29
|
+
arg.each do |key, value|
|
30
|
+
if model && model.association?(key)
|
31
|
+
result += for_association(key, value, "n#{node_num}", model)
|
32
|
+
node_num += 1
|
33
|
+
else
|
34
|
+
result << new_for_key_and_value(model, key, value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
elsif arg.is_a?(String)
|
38
|
+
result << new(:where, arg, args)
|
39
|
+
end
|
40
|
+
result
|
41
|
+
end
|
42
|
+
alias_method :for_node_where_clause, :for_where_clause
|
43
|
+
|
44
|
+
def for_where_not_clause(*args)
|
45
|
+
for_where_clause(*args).each do |link|
|
46
|
+
link.instance_variable_set('@clause', :where_not)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def new_for_key_and_value(model, key, value)
|
51
|
+
key = (key.to_sym == :id ? model.id_property_name : key)
|
52
|
+
|
53
|
+
val = if !model
|
54
|
+
value
|
55
|
+
elsif key == model.id_property_name && value.is_a?(Neo4j::ActiveNode)
|
56
|
+
value.id
|
57
|
+
else
|
58
|
+
converted_value(model, key, value)
|
59
|
+
end
|
60
|
+
|
61
|
+
new(:where, ->(v, _) { {v => {key => val}} })
|
62
|
+
end
|
63
|
+
|
64
|
+
def for_association(name, value, n_string, model)
|
65
|
+
neo_id = value.try(:neo_id) || value
|
66
|
+
fail ArgumentError, "Invalid value for '#{name}' condition" if not neo_id.is_a?(Integer)
|
67
|
+
|
68
|
+
[
|
69
|
+
new(:match, ->(v, _) { "(#{v})#{model.associations[name].arrow_cypher}(#{n_string})" }),
|
70
|
+
new(:where, ->(_, _) { {"ID(#{n_string})" => neo_id.to_i} })
|
71
|
+
]
|
72
|
+
end
|
73
|
+
|
74
|
+
# We don't accept strings here. If you want to use a string, just use where.
|
75
|
+
def for_rel_where_clause(arg, _, association)
|
76
|
+
arg.each_with_object([]) do |(key, value), result|
|
77
|
+
rel_class = association.relationship_class if association.relationship_class
|
78
|
+
val = rel_class ? converted_value(rel_class, key, value) : value
|
79
|
+
result << new(:where, ->(_, rel_var) { {rel_var => {key => val}} })
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def for_rel_order_clause(arg, _)
|
84
|
+
[new(:order, ->(_, v) { arg.is_a?(String) ? arg : {v => arg} })]
|
85
|
+
end
|
86
|
+
|
87
|
+
def for_order_clause(arg, _)
|
88
|
+
[new(:order, ->(v, _) { arg.is_a?(String) ? arg : {v => arg} })]
|
89
|
+
end
|
90
|
+
|
91
|
+
def for_args(model, clause, args, association = nil)
|
92
|
+
if [:where, :where_not].include?(clause) && args[0].is_a?(String) # Better way?
|
93
|
+
[for_arg(model, clause, args[0], *args[1..-1])]
|
94
|
+
elsif clause == :rel_where
|
95
|
+
args.map { |arg| for_arg(model, clause, arg, association) }
|
96
|
+
else
|
97
|
+
args.map { |arg| for_arg(model, clause, arg) }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def for_arg(model, clause, arg, *args)
|
102
|
+
default = [Link.new(clause, arg, *args)]
|
103
|
+
|
104
|
+
Link.for_clause(clause, arg, model, *args) || default
|
105
|
+
rescue NoMethodError
|
106
|
+
default
|
107
|
+
end
|
108
|
+
|
109
|
+
def converted_value(model, key, value)
|
110
|
+
model.declared_properties.value_for_where(key, value)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module ActiveNode
|
3
|
+
module Query
|
4
|
+
module QueryProxyMethods
|
5
|
+
FIRST = 'HEAD'
|
6
|
+
LAST = 'LAST'
|
7
|
+
|
8
|
+
def rels
|
9
|
+
fail 'Cannot get rels without a relationship variable.' if !@rel_var
|
10
|
+
|
11
|
+
pluck(@rel_var)
|
12
|
+
end
|
13
|
+
|
14
|
+
def rel
|
15
|
+
rels.first
|
16
|
+
end
|
17
|
+
|
18
|
+
# Give ability to call `#find` on associations to get a scoped find
|
19
|
+
# Doesn't pass through via `method_missing` because Enumerable has a `#find` method
|
20
|
+
def find(*args)
|
21
|
+
scoping { @model.find(*args) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def first(target = nil)
|
25
|
+
first_and_last(FIRST, target)
|
26
|
+
end
|
27
|
+
|
28
|
+
def last(target = nil)
|
29
|
+
first_and_last(LAST, target)
|
30
|
+
end
|
31
|
+
|
32
|
+
def order_property
|
33
|
+
# This should maybe be based on a setting in the association
|
34
|
+
# rather than a hardcoded `nil`
|
35
|
+
model ? model.id_property_name : nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Integer] number of nodes of this class
|
39
|
+
def count(distinct = nil, target = nil)
|
40
|
+
fail(Neo4j::InvalidParameterError, ':count accepts `distinct` or nil as a parameter') unless distinct.nil? || distinct == :distinct
|
41
|
+
query_with_target(target) do |var|
|
42
|
+
q = distinct.nil? ? var : "DISTINCT #{var}"
|
43
|
+
limited_query = self.query.clause?(:limit) ? self.query.break.with(var) : self.query.reorder
|
44
|
+
limited_query.pluck("count(#{q}) AS #{var}").first
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def size
|
49
|
+
result_cache? ? result_cache_for.length : count
|
50
|
+
end
|
51
|
+
|
52
|
+
delegate :length, to: :to_a
|
53
|
+
|
54
|
+
# TODO: update this with public API methods if/when they are exposed
|
55
|
+
def limit_value
|
56
|
+
return unless self.query.clause?(:limit)
|
57
|
+
limit_clause = self.query.send(:clauses).find { |clause| clause.is_a?(Neo4j::Core::QueryClauses::LimitClause) }
|
58
|
+
limit_clause.instance_variable_get(:@arg)
|
59
|
+
end
|
60
|
+
|
61
|
+
def empty?(target = nil)
|
62
|
+
query_with_target(target) { |var| !self.exists?(nil, var) }
|
63
|
+
end
|
64
|
+
|
65
|
+
alias_method :blank?, :empty?
|
66
|
+
|
67
|
+
# @param [Neo4j::ActiveNode, Neo4j::Node, String] other An instance of a Neo4j.rb model, a Neo4j-core node, or a string uuid
|
68
|
+
# @param [String, Symbol] target An identifier of a link in the Cypher chain
|
69
|
+
# @return [Boolean]
|
70
|
+
def include?(other, target = nil)
|
71
|
+
query_with_target(target) do |var|
|
72
|
+
where_filter = if other.respond_to?(:neo_id)
|
73
|
+
"ID(#{var}) = {other_node_id}"
|
74
|
+
else
|
75
|
+
"#{var}.#{association_id_key} = {other_node_id}"
|
76
|
+
end
|
77
|
+
node_id = other.respond_to?(:neo_id) ? other.neo_id : other
|
78
|
+
self.where(where_filter).params(other_node_id: node_id).query.reorder.return("count(#{var}) as count").first.count > 0
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def exists?(node_condition = nil, target = nil)
|
83
|
+
unless node_condition.is_a?(Integer) || node_condition.is_a?(Hash) || node_condition.nil?
|
84
|
+
fail(Neo4j::InvalidParameterError, ':exists? only accepts neo_ids')
|
85
|
+
end
|
86
|
+
query_with_target(target) do |var|
|
87
|
+
start_q = exists_query_start(node_condition, var)
|
88
|
+
start_q.query.reorder.return("COUNT(#{var}) AS count").first.count > 0
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Shorthand for `MATCH (start)-[r]-(other_node) WHERE ID(other_node) = #{other_node.neo_id}`
|
93
|
+
# The `node` param can be a persisted ActiveNode instance, any string or integer, or nil.
|
94
|
+
# When it's a node, it'll use the object's neo_id, which is fastest. When not nil, it'll figure out the
|
95
|
+
# primary key of that model. When nil, it uses `1 = 2` to prevent matching all records, which is the default
|
96
|
+
# behavior when nil is passed to `where` in QueryProxy.
|
97
|
+
# @param [#neo_id, String, Enumerable] node A node, a string representing a node's ID, or an enumerable of nodes or IDs.
|
98
|
+
# @return [Neo4j::ActiveNode::Query::QueryProxy] A QueryProxy object upon which you can build.
|
99
|
+
def match_to(node)
|
100
|
+
first_node = node.is_a?(Array) ? node.first : node
|
101
|
+
where_arg = if first_node.respond_to?(:neo_id)
|
102
|
+
{neo_id: node.is_a?(Array) ? node.map(&:neo_id) : node}
|
103
|
+
elsif !node.nil?
|
104
|
+
{association_id_key => node.is_a?(Array) ? ids_array(node) : node}
|
105
|
+
else
|
106
|
+
# support for null object pattern
|
107
|
+
'1 = 2'
|
108
|
+
end
|
109
|
+
|
110
|
+
self.where(where_arg)
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# Gives you the first relationship between the last link of a QueryProxy chain and a given node
|
115
|
+
# Shorthand for `MATCH (start)-[r]-(other_node) WHERE ID(other_node) = #{other_node.neo_id} RETURN r`
|
116
|
+
# @param [#neo_id, String, Enumerable] node An object to be sent to `match_to`. See params for that method.
|
117
|
+
# @return A relationship (ActiveRel, CypherRelationship, EmbeddedRelationship) or nil.
|
118
|
+
def first_rel_to(node)
|
119
|
+
self.match_to(node).limit(1).pluck(rel_var).first
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns all relationships across a QueryProxy chain between a given node or array of nodes and the preceeding link.
|
123
|
+
# @param [#neo_id, String, Enumerable] node An object to be sent to `match_to`. See params for that method.
|
124
|
+
# @return An enumerable of relationship objects.
|
125
|
+
def rels_to(node)
|
126
|
+
self.match_to(node).pluck(rel_var)
|
127
|
+
end
|
128
|
+
alias_method :all_rels_to, :rels_to
|
129
|
+
|
130
|
+
# When called, this method returns a single node that satisfies the match specified in the params hash.
|
131
|
+
# If no existing node is found to satisfy the match, one is created or associated as expected.
|
132
|
+
def find_or_create_by(params)
|
133
|
+
fail 'Method invalid when called on Class objects' unless source_object
|
134
|
+
result = self.where(params).first
|
135
|
+
return result unless result.nil?
|
136
|
+
Neo4j::Transaction.run do
|
137
|
+
node = model.find_or_create_by(params)
|
138
|
+
self << node
|
139
|
+
return node
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# A shortcut for attaching a new, optional match to the end of a QueryProxy chain.
|
144
|
+
def optional(association, node_var = nil, rel_var = nil)
|
145
|
+
self.send(association, node_var, rel_var, optional: true)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Takes an Array of ActiveNode models and applies the appropriate WHERE clause
|
149
|
+
# So for a `Teacher` model inheriting from a `Person` model and an `Article` model
|
150
|
+
# if you called .as_models([Teacher, Article])
|
151
|
+
# The where clause would look something like:
|
152
|
+
#
|
153
|
+
# .. code-block:: cypher
|
154
|
+
#
|
155
|
+
# WHERE (node_var:Teacher:Person OR node_var:Article)
|
156
|
+
def as_models(models)
|
157
|
+
where_clause = models.map do |model|
|
158
|
+
"`#{identity}`:" + model.mapped_label_names.map do |mapped_label_name|
|
159
|
+
"`#{mapped_label_name}`"
|
160
|
+
end.join(':')
|
161
|
+
end.join(' OR ')
|
162
|
+
|
163
|
+
where("(#{where_clause})")
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def first_and_last(func, target)
|
169
|
+
new_query, pluck_proc = if self.query.clause?(:order)
|
170
|
+
[self.query.with(identity),
|
171
|
+
proc { |var| "#{func}(COLLECT(#{var})) as #{var}" }]
|
172
|
+
else
|
173
|
+
[self.order(order_property).limit(1),
|
174
|
+
proc { |var| var }]
|
175
|
+
end
|
176
|
+
query_with_target(target) do |var|
|
177
|
+
final_pluck = pluck_proc.call(var)
|
178
|
+
new_query.pluck(final_pluck)
|
179
|
+
end.first
|
180
|
+
end
|
181
|
+
|
182
|
+
# @return [String] The primary key of a the current QueryProxy's model or target class
|
183
|
+
def association_id_key
|
184
|
+
self.association.nil? ? model.primary_key : self.association.target_class.primary_key
|
185
|
+
end
|
186
|
+
|
187
|
+
# @param [Enumerable] node An enumerable of nodes or ids.
|
188
|
+
# @return [Array] An array after having `id` called on each object
|
189
|
+
def ids_array(node)
|
190
|
+
node.first.respond_to?(:id) ? node.map(&:id) : node
|
191
|
+
end
|
192
|
+
|
193
|
+
def query_with_target(target)
|
194
|
+
yield(target || identity)
|
195
|
+
end
|
196
|
+
|
197
|
+
def exists_query_start(condition, target)
|
198
|
+
case condition
|
199
|
+
when Integer
|
200
|
+
self.where("ID(#{target}) = {exists_condition}").params(exists_condition: condition)
|
201
|
+
when Hash
|
202
|
+
self.where(condition.keys.first => condition.values.first)
|
203
|
+
else
|
204
|
+
self
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module ActiveNode
|
3
|
+
module Query
|
4
|
+
module QueryProxyMethodsOfMassUpdating
|
5
|
+
# Updates some attributes of a group of nodes within a QP chain.
|
6
|
+
# The optional argument makes sense only of `updates` is a string.
|
7
|
+
# @param [Hash,String] updates An hash or a string of parameters to be updated.
|
8
|
+
# @param [Hash] params An hash of parameters for the update string. It's ignored if `updates` is an Hash.
|
9
|
+
def update_all(updates, params = {})
|
10
|
+
# Move this to ActiveNode module?
|
11
|
+
update_all_with_query(identity, updates, params)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Updates some attributes of a group of relationships within a QP chain.
|
15
|
+
# The optional argument makes sense only of `updates` is a string.
|
16
|
+
# @param [Hash,String] updates An hash or a string of parameters to be updated.
|
17
|
+
# @param [Hash] params An hash of parameters for the update string. It's ignored if `updates` is an Hash.
|
18
|
+
def update_all_rels(updates, params = {})
|
19
|
+
fail 'Cannot update rels without a relationship variable.' unless @rel_var
|
20
|
+
update_all_with_query(@rel_var, updates, params)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Deletes a group of nodes and relationships within a QP chain. When identifier is omitted, it will remove the last link in the chain.
|
24
|
+
# The optional argument must be a node identifier. A relationship identifier will result in a Cypher Error
|
25
|
+
# @param identifier [String,Symbol] the optional identifier of the link in the chain to delete.
|
26
|
+
def delete_all(identifier = nil)
|
27
|
+
query_with_target(identifier) do |target|
|
28
|
+
begin
|
29
|
+
self.query.with(target).optional_match("(#{target})-[#{target}_rel]-()").delete("#{target}, #{target}_rel").exec
|
30
|
+
rescue Neo4j::Session::CypherError
|
31
|
+
self.query.delete(target).exec
|
32
|
+
end
|
33
|
+
clear_source_object_cache
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Deletes the relationship between a node and its last link in the QueryProxy chain. Executed in the database, callbacks will not run.
|
38
|
+
def delete(node)
|
39
|
+
self.match_to(node).query.delete(rel_var).exec
|
40
|
+
clear_source_object_cache
|
41
|
+
end
|
42
|
+
|
43
|
+
# Deletes the relationships between all nodes for the last step in the QueryProxy chain. Executed in the database, callbacks will not be run.
|
44
|
+
def delete_all_rels
|
45
|
+
return unless start_object && start_object._persisted_obj
|
46
|
+
self.query.delete(rel_var).exec
|
47
|
+
end
|
48
|
+
|
49
|
+
# Deletes the relationships between all nodes for the last step in the QueryProxy chain and replaces them with relationships to the given nodes.
|
50
|
+
# Executed in the database, callbacks will not be run.
|
51
|
+
def replace_with(node_or_nodes)
|
52
|
+
nodes = Array(node_or_nodes)
|
53
|
+
|
54
|
+
self.delete_all_rels
|
55
|
+
nodes.each { |node| self << node }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns all relationships between a node and its last link in the QueryProxy chain, destroys them in Ruby. Callbacks will be run.
|
59
|
+
def destroy(node)
|
60
|
+
self.rels_to(node).map!(&:destroy)
|
61
|
+
clear_source_object_cache
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def update_all_with_query(var_name, updates, params)
|
67
|
+
query = all.query
|
68
|
+
|
69
|
+
case updates
|
70
|
+
when Hash then query.set(var_name => updates).pluck("count(#{var_name})").first
|
71
|
+
when String then query.set(updates).params(params).pluck("count(#{var_name})").first
|
72
|
+
else
|
73
|
+
fail ArgumentError, "Invalid parameter type #{updates.class} for `updates`."
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def clear_source_object_cache
|
78
|
+
self.source_object.clear_association_cache if self.source_object.respond_to?(:clear_association_cache)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module ActiveNode
|
3
|
+
# Helper methods to return Neo4j::Core::Query objects. A query object can be used to successively build a cypher query
|
4
|
+
#
|
5
|
+
# person.query_as(:n).match('n-[:friend]-o').return(o: :name) # Return the names of all the person's friends
|
6
|
+
#
|
7
|
+
module Query
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
# Returns a Query object with the current node matched the specified variable name
|
11
|
+
#
|
12
|
+
# @example Return the names of all of Mike's friends
|
13
|
+
# # Generates: MATCH (mike:Person), mike-[:friend]-friend WHERE ID(mike) = 123 RETURN friend.name
|
14
|
+
# mike.query_as(:mike).match('mike-[:friend]-friend').return(friend: :name)
|
15
|
+
#
|
16
|
+
# @param node_var [Symbol, String] The variable name to specify in the query
|
17
|
+
# @return [Neo4j::Core::Query]
|
18
|
+
def query_as(node_var)
|
19
|
+
self.class.query_as(node_var, false).where("ID(#{node_var})" => self.neo_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Starts a new QueryProxy with the starting identifier set to the given argument and QueryProxy source_object set to the node instance.
|
23
|
+
# This method does not exist within QueryProxy and can only be used to start a new chain.
|
24
|
+
#
|
25
|
+
# @example Start a new QueryProxy chain with the first identifier set manually
|
26
|
+
# # Generates: MATCH (s:`Student`), (l:`Lesson`), s-[rel1:`ENROLLED_IN`]->(l:`Lesson`) WHERE ID(s) = {neo_id_17963}
|
27
|
+
# student.as(:s).lessons(:l)
|
28
|
+
#
|
29
|
+
# @param [String, Symbol] node_var The identifier to use within the QueryProxy object
|
30
|
+
# @return [Neo4j::ActiveNode::Query::QueryProxy]
|
31
|
+
def as(node_var)
|
32
|
+
self.class.query_proxy(node: node_var, source_object: self).match_to(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
# Returns a Query object with all nodes for the model matched as the specified variable name
|
37
|
+
#
|
38
|
+
# @example Return the registration number of all cars owned by a person over the age of 30
|
39
|
+
# # Generates: MATCH (person:Person), person-[:owned]-car WHERE person.age > 30 RETURN car.registration_number
|
40
|
+
# Person.query_as(:person).where('person.age > 30').match('person-[:owned]-car').return(car: :registration_number)
|
41
|
+
#
|
42
|
+
# @param [Symbol, String] var The variable name to specify in the query
|
43
|
+
# @param [Boolean] with_labels Should labels be used to build the match? There are situations (neo_id used to filter,
|
44
|
+
# an early Cypher match has already filtered results) where including labels will degrade performance.
|
45
|
+
# @return [Neo4j::Core::Query]
|
46
|
+
def query_as(var, with_labels = true)
|
47
|
+
query_proxy.query_as(var, with_labels)
|
48
|
+
end
|
49
|
+
|
50
|
+
Neo4j::ActiveNode::Query::QueryProxy::METHODS.each do |method|
|
51
|
+
define_method(method) do |*args|
|
52
|
+
self.query_proxy.send(method, *args)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def query_proxy(options = {})
|
57
|
+
Neo4j::ActiveNode::Query::QueryProxy.new(self, nil, options)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Start a new QueryProxy with the starting identifier set to the given argument.
|
61
|
+
# This method does not exist within QueryProxy, it can only be called at the class level to create a new QP object.
|
62
|
+
# To set an identifier within a QueryProxy chain, give it as the first argument to a chained association.
|
63
|
+
#
|
64
|
+
# @example Start a new QueryProxy where the first identifier is set manually.
|
65
|
+
# # Generates: MATCH (s:`Student`), (result_lessons:`Lesson`), s-[rel1:`ENROLLED_IN`]->(result_lessons:`Lesson`)
|
66
|
+
# Student.as(:s).lessons
|
67
|
+
#
|
68
|
+
# @param [String, Symbol] node_var A string or symbol to use as the starting identifier.
|
69
|
+
# @return [Neo4j::ActiveNode::Query::QueryProxy]
|
70
|
+
def as(node_var)
|
71
|
+
query_proxy(node: node_var, context: self.name)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Neo4j
|
2
|
+
module ActiveNode
|
3
|
+
module QueryMethods
|
4
|
+
def exists?(node_condition = nil)
|
5
|
+
unless node_condition.is_a?(Integer) || node_condition.is_a?(Hash) || node_condition.nil?
|
6
|
+
fail(Neo4j::InvalidParameterError, ':exists? only accepts ids or conditions')
|
7
|
+
end
|
8
|
+
query_start = exists_query_start(node_condition)
|
9
|
+
start_q = query_start.respond_to?(:query_as) ? query_start.query_as(:n) : query_start
|
10
|
+
start_q.return('COUNT(n) AS count').first.count > 0
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns the first node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
|
14
|
+
def first
|
15
|
+
self.query_as(:n).limit(1).order(n: primary_key).pluck(:n).first
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the last node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
|
19
|
+
def last
|
20
|
+
self.query_as(:n).limit(1).order(n: {primary_key => :desc}).pluck(:n).first
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Integer] number of nodes of this class
|
24
|
+
def count(distinct = nil)
|
25
|
+
fail(Neo4j::InvalidParameterError, ':count accepts `distinct` or nil as a parameter') unless distinct.nil? || distinct == :distinct
|
26
|
+
q = distinct.nil? ? 'n' : 'DISTINCT n'
|
27
|
+
self.query_as(:n).return("count(#{q}) AS count").first.count
|
28
|
+
end
|
29
|
+
|
30
|
+
alias_method :size, :count
|
31
|
+
alias_method :length, :count
|
32
|
+
|
33
|
+
def empty?
|
34
|
+
!self.all.exists?
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :blank?, :empty?
|
38
|
+
|
39
|
+
def find_in_batches(options = {})
|
40
|
+
self.query_as(:n).return(:n).find_in_batches(:n, primary_key, options) do |batch|
|
41
|
+
yield batch.map(&:n)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_each(options = {})
|
46
|
+
self.query_as(:n).return(:n).find_each(:n, primary_key, options) do |batch|
|
47
|
+
yield batch.n
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def exists_query_start(node_condition)
|
54
|
+
case node_condition
|
55
|
+
when Integer
|
56
|
+
self.query_as(:n).where('ID(n)' => node_condition)
|
57
|
+
when Hash
|
58
|
+
self.where(node_condition.keys.first => node_condition.values.first)
|
59
|
+
else
|
60
|
+
self.query_as(:n)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|