neo4j 4.1.5 → 5.0.0.rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +584 -0
- data/CONTRIBUTORS +7 -28
- data/Gemfile +6 -1
- data/README.md +54 -8
- data/lib/neo4j.rb +5 -0
- data/lib/neo4j/active_node.rb +1 -0
- data/lib/neo4j/active_node/dependent/association_methods.rb +35 -17
- data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +21 -19
- data/lib/neo4j/active_node/has_n.rb +377 -132
- data/lib/neo4j/active_node/has_n/association.rb +77 -38
- data/lib/neo4j/active_node/id_property.rb +46 -28
- data/lib/neo4j/active_node/initialize.rb +18 -6
- data/lib/neo4j/active_node/labels.rb +69 -35
- data/lib/neo4j/active_node/node_wrapper.rb +37 -30
- data/lib/neo4j/active_node/orm_adapter.rb +5 -4
- data/lib/neo4j/active_node/persistence.rb +53 -10
- data/lib/neo4j/active_node/property.rb +13 -5
- data/lib/neo4j/active_node/query.rb +11 -10
- data/lib/neo4j/active_node/query/query_proxy.rb +126 -153
- data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +15 -25
- data/lib/neo4j/active_node/query/query_proxy_link.rb +89 -0
- data/lib/neo4j/active_node/query/query_proxy_methods.rb +72 -19
- data/lib/neo4j/active_node/query_methods.rb +3 -1
- data/lib/neo4j/active_node/scope.rb +17 -21
- data/lib/neo4j/active_node/validations.rb +8 -2
- data/lib/neo4j/active_rel/initialize.rb +1 -2
- data/lib/neo4j/active_rel/persistence.rb +21 -33
- data/lib/neo4j/active_rel/property.rb +4 -2
- data/lib/neo4j/active_rel/types.rb +20 -8
- data/lib/neo4j/config.rb +16 -6
- data/lib/neo4j/core/query.rb +2 -2
- data/lib/neo4j/errors.rb +10 -0
- data/lib/neo4j/migration.rb +57 -46
- data/lib/neo4j/paginated.rb +3 -1
- data/lib/neo4j/railtie.rb +26 -14
- data/lib/neo4j/shared.rb +7 -1
- data/lib/neo4j/shared/declared_property.rb +62 -0
- data/lib/neo4j/shared/declared_property_manager.rb +150 -0
- data/lib/neo4j/shared/persistence.rb +15 -8
- data/lib/neo4j/shared/property.rb +64 -49
- data/lib/neo4j/shared/rel_type_converters.rb +13 -12
- data/lib/neo4j/shared/serialized_properties.rb +0 -15
- data/lib/neo4j/shared/type_converters.rb +53 -47
- data/lib/neo4j/shared/typecaster.rb +49 -0
- data/lib/neo4j/version.rb +1 -1
- data/lib/rails/generators/neo4j/model/model_generator.rb +3 -3
- data/lib/rails/generators/neo4j_generator.rb +5 -12
- data/neo4j.gemspec +4 -3
- metadata +30 -11
- data/CHANGELOG +0 -545
@@ -1,51 +1,58 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
1
3
|
class Neo4j::Node
|
2
4
|
# The wrapping process is what transforms a raw CypherNode or EmbeddedNode from Neo4j::Core into a healthy ActiveNode (or ActiveRel) object.
|
3
5
|
module Wrapper
|
4
6
|
# this is a plugin in the neo4j-core so that the Ruby wrapper will be wrapped around the Neo4j::Node objects
|
5
7
|
def wrapper
|
6
|
-
|
7
|
-
|
8
|
-
return self unless most_concrete_class
|
9
|
-
wrapped_node = most_concrete_class.new
|
10
|
-
wrapped_node.init_on_load(self, self.props)
|
11
|
-
wrapped_node
|
12
|
-
end
|
8
|
+
found_class = class_to_wrap
|
9
|
+
return self if not found_class
|
13
10
|
|
14
|
-
|
15
|
-
|
11
|
+
found_class.new.tap do |wrapped_node|
|
12
|
+
wrapped_node.init_on_load(self, self.props)
|
13
|
+
end
|
16
14
|
end
|
17
15
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
checked_labels_set.add(label_name)
|
16
|
+
def class_to_wrap
|
17
|
+
load_classes_from_labels
|
18
|
+
(named_class || ::Neo4j::ActiveNode::Labels.model_for_labels(labels)).tap do |model_class|
|
19
|
+
Neo4j::Node::Wrapper.populate_constants_for_labels_cache(model_class, labels)
|
23
20
|
end
|
24
21
|
end
|
25
22
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
else
|
31
|
-
wrappers = _class_wrappers
|
32
|
-
return self if wrappers.nil?
|
33
|
-
wrapper_classes = wrappers.map { |w| Neo4j::ActiveNode::Labels._wrapped_labels[w] }
|
34
|
-
wrapper_classes.sort.first
|
35
|
-
end
|
23
|
+
private
|
24
|
+
|
25
|
+
def load_classes_from_labels
|
26
|
+
labels.each { |label| Neo4j::Node::Wrapper.constant_for_label(label) }
|
36
27
|
end
|
37
28
|
|
38
|
-
|
39
|
-
|
29
|
+
# Only load classes once for performance
|
30
|
+
CONSTANTS_FOR_LABELS_CACHE = {}
|
31
|
+
|
32
|
+
def self.constant_for_label(label)
|
33
|
+
CONSTANTS_FOR_LABELS_CACHE[label] || CONSTANTS_FOR_LABELS_CACHE[label] = constantized_label(label)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.constantized_label(label)
|
37
|
+
"#{association_model_namespace}::#{label}".constantize
|
40
38
|
rescue NameError
|
41
39
|
nil
|
42
40
|
end
|
43
41
|
|
44
|
-
def
|
45
|
-
labels.
|
46
|
-
|
47
|
-
Neo4j::ActiveNode::Labels._wrapped_labels[label_name].class == Class
|
42
|
+
def self.populate_constants_for_labels_cache(model_class, labels)
|
43
|
+
labels.each do |label|
|
44
|
+
CONSTANTS_FOR_LABELS_CACHE[label] = model_class if CONSTANTS_FOR_LABELS_CACHE[label].nil?
|
48
45
|
end
|
49
46
|
end
|
47
|
+
|
48
|
+
def self.association_model_namespace
|
49
|
+
Neo4j::Config.association_model_namespace_string
|
50
|
+
end
|
51
|
+
|
52
|
+
def named_class
|
53
|
+
property = Neo4j::Config.class_name_property
|
54
|
+
|
55
|
+
Neo4j::Node::Wrapper.constant_for_label(self.props[property]) if self.props.is_a?(Hash) && self.props.key?(property)
|
56
|
+
end
|
50
57
|
end
|
51
58
|
end
|
@@ -28,7 +28,7 @@ module Neo4j
|
|
28
28
|
|
29
29
|
# Get an instance by id of the model
|
30
30
|
def get(id)
|
31
|
-
klass.
|
31
|
+
klass.find_by(klass.id_property_name => wrap_key(id))
|
32
32
|
end
|
33
33
|
|
34
34
|
# Find the first instance matching conditions
|
@@ -72,9 +72,10 @@ module Neo4j
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def extract_id!(conditions)
|
75
|
-
|
76
|
-
|
77
|
-
|
75
|
+
id = conditions.delete(:id)
|
76
|
+
return if not id
|
77
|
+
|
78
|
+
conditions[klass.id_property_name.to_sym] = id
|
78
79
|
end
|
79
80
|
end
|
80
81
|
end
|
@@ -10,17 +10,21 @@ module Neo4j::ActiveNode
|
|
10
10
|
end
|
11
11
|
|
12
12
|
extend ActiveSupport::Concern
|
13
|
+
extend Forwardable
|
13
14
|
include Neo4j::Shared::Persistence
|
14
15
|
|
15
16
|
# Saves the model.
|
16
17
|
#
|
17
18
|
# If the model is new a record gets created in the database, otherwise the existing record gets updated.
|
18
19
|
# If perform_validation is true validations run.
|
19
|
-
# If any of them fail the action is cancelled and save returns false.
|
20
|
-
#
|
20
|
+
# If any of them fail the action is cancelled and save returns false.
|
21
|
+
# If the flag is false validations are bypassed altogether.
|
22
|
+
# See ActiveRecord::Validations for more information.
|
23
|
+
# There's a series of callbacks associated with save.
|
24
|
+
# If any of the before_* callbacks return false the action is cancelled and save returns false.
|
21
25
|
def save(*)
|
22
26
|
update_magic_properties
|
23
|
-
|
27
|
+
association_proxy_cache.clear
|
24
28
|
create_or_update
|
25
29
|
end
|
26
30
|
|
@@ -45,13 +49,11 @@ module Neo4j::ActiveNode
|
|
45
49
|
create_magic_properties
|
46
50
|
set_timestamps
|
47
51
|
create_magic_properties
|
48
|
-
properties = convert_properties_to :db, props
|
52
|
+
properties = self.class.declared_property_manager.convert_properties_to(self, :db, props)
|
49
53
|
node = _create_node(properties)
|
50
54
|
init_on_load(node, node.props)
|
51
55
|
send_props(@relationship_props) if @relationship_props
|
52
56
|
@relationship_props = nil
|
53
|
-
# Neo4j::IdentityMap.add(node, self)
|
54
|
-
# write_changed_relationships
|
55
57
|
true
|
56
58
|
end
|
57
59
|
|
@@ -68,7 +70,7 @@ module Neo4j::ActiveNode
|
|
68
70
|
# Creates and saves a new node
|
69
71
|
# @param [Hash] props the properties the new node should have
|
70
72
|
def create(props = {})
|
71
|
-
association_props = extract_association_attributes!(props)
|
73
|
+
association_props = extract_association_attributes!(props) || {}
|
72
74
|
|
73
75
|
new(props).tap do |obj|
|
74
76
|
yield obj if block_given?
|
@@ -82,7 +84,7 @@ module Neo4j::ActiveNode
|
|
82
84
|
# Same as #create, but raises an error if there is a problem during save.
|
83
85
|
def create!(*args)
|
84
86
|
props = args[0] || {}
|
85
|
-
association_props = extract_association_attributes!(props)
|
87
|
+
association_props = extract_association_attributes!(props) || {}
|
86
88
|
|
87
89
|
new(*args).tap do |o|
|
88
90
|
yield o if block_given?
|
@@ -93,6 +95,21 @@ module Neo4j::ActiveNode
|
|
93
95
|
end
|
94
96
|
end
|
95
97
|
|
98
|
+
def merge(attributes)
|
99
|
+
neo4j_session.query.merge(n: {self.mapped_label_names => attributes})
|
100
|
+
.on_create_set(n: on_create_props(attributes))
|
101
|
+
.on_match_set(n: on_match_props)
|
102
|
+
.pluck(:n).first
|
103
|
+
end
|
104
|
+
|
105
|
+
def find_or_create(find_attributes, set_attributes = {})
|
106
|
+
on_create_attributes = set_attributes.merge(on_create_props(find_attributes))
|
107
|
+
on_match_attributes = set_attributes.merge(on_match_props)
|
108
|
+
neo4j_session.query.merge(n: {self.mapped_label_names => find_attributes})
|
109
|
+
.on_create_set(n: on_create_attributes).on_match_set(n: on_match_attributes)
|
110
|
+
.pluck(:n).first
|
111
|
+
end
|
112
|
+
|
96
113
|
# Finds the first node with the given attributes, or calls create if none found
|
97
114
|
def find_or_create_by(attributes, &block)
|
98
115
|
find_by(attributes) || create(attributes, &block)
|
@@ -106,8 +123,34 @@ module Neo4j::ActiveNode
|
|
106
123
|
def load_entity(id)
|
107
124
|
Neo4j::Node.load(id)
|
108
125
|
end
|
109
|
-
end
|
110
126
|
|
111
|
-
|
127
|
+
private
|
128
|
+
|
129
|
+
def on_create_props(find_attributes)
|
130
|
+
{id_property_name => id_prop_val(find_attributes)}.tap do |props|
|
131
|
+
now = DateTime.now.to_i
|
132
|
+
set_props_timestamp!('created_at', props, now)
|
133
|
+
set_props_timestamp!('updated_at', props, now)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# The process of creating custom id_property values is different from auto uuids. This adapts to that, calls the appropriate method,
|
138
|
+
# and raises an error if it fails.
|
139
|
+
def id_prop_val(find_attributes)
|
140
|
+
custom_uuid_method = id_property_info[:type][:on]
|
141
|
+
id_prop_val = custom_uuid_method ? self.new(find_attributes).send(custom_uuid_method) : default_properties[id_property_name].call
|
142
|
+
fail 'Unable to create custom id property' if id_prop_val.nil?
|
143
|
+
id_prop_val
|
144
|
+
end
|
145
|
+
|
146
|
+
def on_match_props
|
147
|
+
set_props_timestamp!('updated_at')
|
148
|
+
end
|
149
|
+
|
150
|
+
def set_props_timestamp!(key_name, props = {}, stamp = DateTime.now.to_i)
|
151
|
+
props[key_name.to_sym] = stamp if attributes_nil_hash.key?(key_name)
|
152
|
+
props
|
153
|
+
end
|
154
|
+
end
|
112
155
|
end
|
113
156
|
end
|
@@ -5,18 +5,26 @@ module Neo4j::ActiveNode
|
|
5
5
|
|
6
6
|
def initialize(attributes = {}, options = {})
|
7
7
|
super(attributes, options)
|
8
|
-
|
9
|
-
send_props(@relationship_props) if
|
8
|
+
@attributes ||= self.class.attributes_nil_hash.dup
|
9
|
+
send_props(@relationship_props) if _persisted_obj && !@relationship_props.nil?
|
10
10
|
end
|
11
11
|
|
12
12
|
module ClassMethods
|
13
|
-
# Extracts keys from attributes hash which are
|
13
|
+
# Extracts keys from attributes hash which are associations of the model
|
14
14
|
# TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save?
|
15
15
|
def extract_association_attributes!(attributes)
|
16
|
-
attributes
|
17
|
-
|
16
|
+
return unless contains_association?(attributes)
|
17
|
+
attributes.each_with_object({}) do |(key, _), result|
|
18
|
+
result[key] = attributes.delete(key) if self.association?(key)
|
18
19
|
end
|
19
20
|
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def contains_association?(attributes)
|
25
|
+
attributes.each_key { |key| return true if associations_keys.include?(key) }
|
26
|
+
false
|
27
|
+
end
|
20
28
|
end
|
21
29
|
end
|
22
30
|
end
|
@@ -16,10 +16,10 @@ module Neo4j
|
|
16
16
|
# @param var [Symbol, String] The variable name to specify in the query
|
17
17
|
# @return [Neo4j::Core::Query]
|
18
18
|
def query_as(node_var)
|
19
|
-
self.class.query_as(node_var).where("ID(#{node_var})" => self.neo_id)
|
19
|
+
self.class.query_as(node_var, false).where("ID(#{node_var})" => self.neo_id)
|
20
20
|
end
|
21
21
|
|
22
|
-
# Starts a new QueryProxy with the starting identifier set to the given argument and QueryProxy
|
22
|
+
# Starts a new QueryProxy with the starting identifier set to the given argument and QueryProxy source_object set to the node instance.
|
23
23
|
# This method does not exist within QueryProxy and can only be used to start a new chain.
|
24
24
|
#
|
25
25
|
# @example Start a new QueryProxy chain with the first identifier set manually
|
@@ -29,7 +29,7 @@ module Neo4j
|
|
29
29
|
# @param [String, Symbol] node_var The identifier to use within the QueryProxy object
|
30
30
|
# @return [Neo4j::ActiveNode::Query::QueryProxy]
|
31
31
|
def as(node_var)
|
32
|
-
self.class.query_proxy(node: node_var,
|
32
|
+
self.class.query_proxy(node: node_var, source_object: self).match_to(self)
|
33
33
|
end
|
34
34
|
|
35
35
|
module ClassMethods
|
@@ -39,17 +39,18 @@ module Neo4j
|
|
39
39
|
# # Generates: MATCH (person:Person), person-[:owned]-car WHERE person.age > 30 RETURN car.registration_number
|
40
40
|
# Person.query_as(:person).where('person.age > 30').match('person-[:owned]-car').return(car: :registration_number)
|
41
41
|
#
|
42
|
-
# @param
|
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.
|
43
45
|
# @return [Neo4j::Core::Query]
|
44
|
-
def query_as(var)
|
45
|
-
query_proxy.query_as(var)
|
46
|
+
def query_as(var, with_labels = true)
|
47
|
+
query_proxy.query_as(var, with_labels)
|
46
48
|
end
|
47
49
|
|
48
50
|
Neo4j::ActiveNode::Query::QueryProxy::METHODS.each do |method|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end}, __FILE__, __LINE__)
|
51
|
+
define_method(method) do |*args|
|
52
|
+
self.query_proxy.send(method, *args)
|
53
|
+
end
|
53
54
|
end
|
54
55
|
|
55
56
|
def query_proxy(options = {})
|
@@ -9,7 +9,7 @@ module Neo4j
|
|
9
9
|
|
10
10
|
# The most recent node to start a QueryProxy chain.
|
11
11
|
# Will be nil when using QueryProxy chains on class methods.
|
12
|
-
attr_reader :
|
12
|
+
attr_reader :source_object, :association, :model, :starting_query
|
13
13
|
|
14
14
|
# QueryProxy is ActiveNode's Cypher DSL. While the name might imply that it creates queries in a general sense,
|
15
15
|
# it is actually referring to <tt>Neo4j::Core::Query</tt>, which is a pure Ruby Cypher DSL provided by the <tt>neo4j-core</tt> gem.
|
@@ -28,7 +28,7 @@ module Neo4j
|
|
28
28
|
# * node_var: A string or symbol to be used by Cypher within its query string as an identifier
|
29
29
|
# * rel_var: Same as above but pertaining to a relationship identifier
|
30
30
|
# * session: The session to be used for this query
|
31
|
-
# *
|
31
|
+
# * source_object: The node instance at the start of the QueryProxy chain
|
32
32
|
# * query_proxy: An existing QueryProxy chain upon which this new object should be built
|
33
33
|
#
|
34
34
|
# QueryProxy objects are evaluated lazily.
|
@@ -37,17 +37,26 @@ module Neo4j
|
|
37
37
|
@association = association
|
38
38
|
@context = options.delete(:context)
|
39
39
|
@options = options
|
40
|
-
|
40
|
+
|
41
|
+
@node_var, @session, @source_object, @starting_query, @optional, @start_object, @query_proxy, @chain_level =
|
42
|
+
options.values_at(:node, :session, :source_object, :starting_query, :optional, :start_object, :query_proxy, :chain_level)
|
43
|
+
|
44
|
+
@match_type = @optional ? :optional_match : :match
|
45
|
+
|
41
46
|
@rel_var = options[:rel] || _rel_chain_var
|
42
|
-
|
43
|
-
@caller = options[:caller]
|
44
|
-
@chain_level = options[:chain_level]
|
47
|
+
|
45
48
|
@chain = []
|
46
|
-
@
|
47
|
-
|
48
|
-
|
49
|
+
@params = @query_proxy ? @query_proxy.instance_variable_get('@params') : {}
|
50
|
+
end
|
51
|
+
|
52
|
+
def inspect
|
53
|
+
clear, yellow, cyan = %W(\e[0m \e[33m \e[36m)
|
54
|
+
|
55
|
+
"<QueryProxy #{cyan}#{@context}#{clear} CYPHER: #{yellow}#{self.to_cypher.inspect}#{clear}>"
|
49
56
|
end
|
50
57
|
|
58
|
+
attr_reader :start_object, :query_proxy
|
59
|
+
|
51
60
|
# The current node identifier on deck, so to speak. It is the object that will be returned by calling `each` and the last node link
|
52
61
|
# in the QueryProxy chain.
|
53
62
|
attr_reader :node_var
|
@@ -65,9 +74,7 @@ module Neo4j
|
|
65
74
|
end
|
66
75
|
|
67
76
|
def params(params)
|
68
|
-
|
69
|
-
new_query._add_params(params)
|
70
|
-
end
|
77
|
+
new_link.tap { |new_query| new_query._add_params(params) }
|
71
78
|
end
|
72
79
|
|
73
80
|
# Like calling #query_as, but for when you don't care about the variable name
|
@@ -79,22 +86,31 @@ module Neo4j
|
|
79
86
|
# and work with it from the more powerful (but less friendly) Neo4j::Core::Query.
|
80
87
|
# @param [String,Symbol] var The identifier to use for node at this link of the QueryProxy chain.
|
81
88
|
# student.lessons.query_as(:l).with('your cypher here...')
|
82
|
-
def query_as(var)
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
else
|
88
|
-
starting_query ? (starting_query & _query_model_as(var)) : _query_model_as(var)
|
89
|
-
end
|
90
|
-
# Build a query chain via the chain, return the result
|
91
|
-
result_query = @chain.inject(base_query.params(@params)) do |query, (method, arg)|
|
92
|
-
query.send(method, arg.respond_to?(:call) ? arg.call(var) : arg)
|
89
|
+
def query_as(var, with_label = true)
|
90
|
+
result_query = @chain.inject(base_query(var, with_label).params(@params)) do |query, link|
|
91
|
+
args = link.args(var, rel_var)
|
92
|
+
|
93
|
+
args.is_a?(Array) ? query.send(link.clause, *args) : query.send(link.clause, args)
|
93
94
|
end
|
94
95
|
|
95
96
|
result_query.tap { |query| query.proxy_chain_level = _chain_level }
|
96
97
|
end
|
97
98
|
|
99
|
+
def base_query(var, with_labels = true)
|
100
|
+
if @association
|
101
|
+
chain_var = _association_chain_var
|
102
|
+
(_association_query_start(chain_var) & _query).send(@match_type,
|
103
|
+
"#{chain_var}#{_association_arrow}(#{var}#{_model_label_string})")
|
104
|
+
else
|
105
|
+
starting_query ? (starting_query & _query_model_as(var, with_labels)) : _query_model_as(var, with_labels)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def _model_label_string
|
110
|
+
return if !@model
|
111
|
+
@model.mapped_label_names.map { |label_name| ":`#{label_name}`" }.join
|
112
|
+
end
|
113
|
+
|
98
114
|
# Scope all queries to the current scope.
|
99
115
|
#
|
100
116
|
# Comment.where(post_id: 1).scoping do
|
@@ -105,7 +121,8 @@ module Neo4j
|
|
105
121
|
# Please check unscoped if you want to remove all previous scopes (including
|
106
122
|
# the default_scope) during the execution of a block.
|
107
123
|
def scoping
|
108
|
-
previous
|
124
|
+
previous = @model.current_scope
|
125
|
+
@model.current_scope = self
|
109
126
|
yield
|
110
127
|
ensure
|
111
128
|
@model.current_scope = previous
|
@@ -114,10 +131,7 @@ module Neo4j
|
|
114
131
|
METHODS = %w(where rel_where order skip limit)
|
115
132
|
|
116
133
|
METHODS.each do |method|
|
117
|
-
|
118
|
-
def #{method}(*args)
|
119
|
-
build_deeper_query_proxy(:#{method}, args)
|
120
|
-
end}, __FILE__, __LINE__)
|
134
|
+
define_method(method) { |*args| build_deeper_query_proxy(method.to_sym, args) }
|
121
135
|
end
|
122
136
|
# Since there is a rel_where method, it seems only natural for there to be node_where
|
123
137
|
alias_method :node_where, :where
|
@@ -151,35 +165,53 @@ module Neo4j
|
|
151
165
|
end
|
152
166
|
|
153
167
|
def create(other_nodes, properties)
|
154
|
-
fail 'Can only create
|
155
|
-
other_nodes =
|
168
|
+
fail 'Can only create relationships on associations' if !@association
|
169
|
+
other_nodes = _nodeify!(*other_nodes)
|
170
|
+
|
156
171
|
properties = @association.inject_classname(properties)
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
other_node
|
172
|
+
|
173
|
+
Neo4j::Transaction.run do
|
174
|
+
other_nodes.each do |other_node|
|
175
|
+
other_node.save unless other_node.neo_id
|
176
|
+
|
177
|
+
return false if @association.perform_callback(@start_object, other_node, :before) == false
|
178
|
+
|
179
|
+
@start_object.association_proxy_cache.clear
|
180
|
+
|
181
|
+
_create_relationship(other_node, properties)
|
182
|
+
|
183
|
+
@association.perform_callback(@start_object, other_node, :after)
|
163
184
|
end
|
164
|
-
end
|
185
|
+
end
|
186
|
+
end
|
165
187
|
|
166
|
-
|
167
|
-
|
168
|
-
# Neo4j::Transaction.run do
|
169
|
-
other_node.save unless other_node.neo_id
|
188
|
+
def rels
|
189
|
+
fail 'Cannot get rels without a relationship variable.' if !@rel_var
|
170
190
|
|
171
|
-
|
191
|
+
pluck(@rel_var)
|
192
|
+
end
|
172
193
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
194
|
+
def rel
|
195
|
+
rels.first
|
196
|
+
end
|
197
|
+
|
198
|
+
def _nodeify!(*args)
|
199
|
+
other_nodes = [args].flatten!.map! do |arg|
|
200
|
+
(arg.is_a?(Integer) || arg.is_a?(String)) ? @model.find_by(@model.id_property_name => arg) : arg
|
201
|
+
end.compact
|
179
202
|
|
180
|
-
|
181
|
-
|
203
|
+
if @model && other_nodes.any? { |other_node| !other_node.is_a?(@model) }
|
204
|
+
fail ArgumentError, "Node must be of the association's class when model is specified"
|
182
205
|
end
|
206
|
+
|
207
|
+
other_nodes
|
208
|
+
end
|
209
|
+
|
210
|
+
def _create_relationship(other_node_or_nodes, properties)
|
211
|
+
_session.query(context: @options[:context])
|
212
|
+
.match(:start, :end)
|
213
|
+
.where(start: {neo_id: @start_object}, end: {neo_id: other_node_or_nodes})
|
214
|
+
.send(association.create_method, "start#{_association_arrow(properties, true)}end").exec
|
183
215
|
end
|
184
216
|
|
185
217
|
def read_attribute_for_serialization(*args)
|
@@ -190,23 +222,37 @@ module Neo4j
|
|
190
222
|
# This allows us to define class functions for reusable query chaining or for end-of-query aggregation/summarizing
|
191
223
|
def method_missing(method_name, *args, &block)
|
192
224
|
if @model && @model.respond_to?(method_name)
|
193
|
-
args[2] = self if @model.association?(method_name) || @model.scope?(method_name)
|
194
225
|
scoping { @model.public_send(method_name, *args, &block) }
|
195
226
|
else
|
196
227
|
super
|
197
228
|
end
|
198
229
|
end
|
199
230
|
|
231
|
+
def respond_to?(method_name)
|
232
|
+
(@model && @model.respond_to?(method_name)) || super
|
233
|
+
end
|
234
|
+
|
235
|
+
# Give ability to call `#find` on associations to get a scoped find
|
236
|
+
# Doesn't pass through via `method_missing` because Enumerable has a `#find` method
|
237
|
+
def find(*args)
|
238
|
+
scoping { @model.find(*args) }
|
239
|
+
end
|
240
|
+
|
200
241
|
def optional?
|
201
242
|
@optional == true
|
202
243
|
end
|
203
244
|
|
204
245
|
attr_reader :context
|
205
246
|
|
247
|
+
def new_link(node_var = nil)
|
248
|
+
self.clone.tap do |new_query_proxy|
|
249
|
+
new_query_proxy.instance_variable_set('@node_var', node_var) if node_var
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
206
253
|
protected
|
207
254
|
|
208
255
|
# Methods are underscored to prevent conflict with user class methods
|
209
|
-
|
210
256
|
def _add_params(params)
|
211
257
|
@params = @params.merge(params)
|
212
258
|
end
|
@@ -215,29 +261,32 @@ module Neo4j
|
|
215
261
|
@chain += links
|
216
262
|
end
|
217
263
|
|
218
|
-
def _query_model_as(var)
|
219
|
-
|
220
|
-
label = @model.respond_to?(:mapped_label_name) ? @model.mapped_label_name : @model
|
221
|
-
{var => label}
|
222
|
-
else
|
223
|
-
var
|
224
|
-
end
|
225
|
-
_session.query(context: @context).send(_match_type, match_arg)
|
264
|
+
def _query_model_as(var, with_labels = true)
|
265
|
+
_query.send(@match_type, _match_arg(var, with_labels))
|
226
266
|
end
|
227
267
|
|
228
|
-
#
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
label.downcase!.tr!(':', '')
|
235
|
-
label.to_sym
|
268
|
+
# @param [String, Symbol] var The Cypher identifier to use within the match string
|
269
|
+
# @param [Boolean] with_labels Send "true" to include model labels where possible.
|
270
|
+
def _match_arg(var, with_labels)
|
271
|
+
if @model && with_labels != false
|
272
|
+
labels = @model.respond_to?(:mapped_label_names) ? _model_label_string : @model
|
273
|
+
{var => labels}
|
236
274
|
else
|
237
|
-
|
275
|
+
var
|
238
276
|
end
|
239
277
|
end
|
240
278
|
|
279
|
+
def _query
|
280
|
+
_session.query(context: @context)
|
281
|
+
end
|
282
|
+
|
283
|
+
# TODO: Refactor this. Too much happening here.
|
284
|
+
def _result_string
|
285
|
+
s = (self.association && self.association.name) || (self.model && self.model.name) || ''
|
286
|
+
|
287
|
+
s ? "result_#{s}".downcase.tr(':', '').to_sym : :result
|
288
|
+
end
|
289
|
+
|
241
290
|
def _session
|
242
291
|
@session || (@model && @model.neo4j_session)
|
243
292
|
end
|
@@ -247,30 +296,22 @@ module Neo4j
|
|
247
296
|
end
|
248
297
|
|
249
298
|
def _chain_level
|
250
|
-
|
251
|
-
query_proxy._chain_level + 1
|
252
|
-
elsif @chain_level
|
253
|
-
@chain_level + 1
|
254
|
-
else
|
255
|
-
1
|
256
|
-
end
|
299
|
+
(@query_proxy ? @query_proxy._chain_level : (@chain_level || 0)) + 1
|
257
300
|
end
|
258
301
|
|
259
302
|
def _association_chain_var
|
260
|
-
if start_object
|
303
|
+
if start_object
|
261
304
|
:"#{start_object.class.name.gsub('::', '_').downcase}#{start_object.neo_id}"
|
262
|
-
elsif
|
263
|
-
query_proxy.node_var || :"node#{_chain_level}"
|
305
|
+
elsif @query_proxy
|
306
|
+
@query_proxy.node_var || :"node#{_chain_level}"
|
264
307
|
else
|
265
308
|
fail 'Crazy error' # TODO: Better error
|
266
309
|
end
|
267
310
|
end
|
268
311
|
|
269
312
|
def _association_query_start(var)
|
270
|
-
if
|
271
|
-
|
272
|
-
elsif query_proxy = @options[:query_proxy]
|
273
|
-
query_proxy.query_as(var)
|
313
|
+
if object = (start_object || @query_proxy)
|
314
|
+
object.query_as(var)
|
274
315
|
else
|
275
316
|
fail 'Crazy error' # TODO: Better error
|
276
317
|
end
|
@@ -280,82 +321,14 @@ module Neo4j
|
|
280
321
|
:"rel#{_chain_level - 1}"
|
281
322
|
end
|
282
323
|
|
283
|
-
def _match_type
|
284
|
-
@optional ? :optional_match : :match
|
285
|
-
end
|
286
|
-
|
287
324
|
attr_writer :context
|
288
325
|
|
289
326
|
private
|
290
327
|
|
291
|
-
def create_method
|
292
|
-
association.unique? ? :create_unique : :create
|
293
|
-
end
|
294
|
-
|
295
328
|
def build_deeper_query_proxy(method, args)
|
296
|
-
|
297
|
-
args.each
|
298
|
-
new_query._add_links(links_for_arg(method, arg))
|
299
|
-
end
|
300
|
-
end
|
301
|
-
end
|
302
|
-
|
303
|
-
def links_for_arg(method, arg)
|
304
|
-
method_to_call = "links_for_#{method}_arg"
|
305
|
-
|
306
|
-
default = [[method, arg]]
|
307
|
-
|
308
|
-
self.send(method_to_call, arg) || default
|
309
|
-
rescue NoMethodError
|
310
|
-
default
|
311
|
-
end
|
312
|
-
|
313
|
-
def links_for_where_arg(arg)
|
314
|
-
node_num = 1
|
315
|
-
result = []
|
316
|
-
if arg.is_a?(Hash)
|
317
|
-
arg.each do |key, value|
|
318
|
-
if @model && @model.association?(key)
|
319
|
-
result += links_for_association(key, value, "n#{node_num}")
|
320
|
-
|
321
|
-
node_num += 1
|
322
|
-
else
|
323
|
-
result << [:where, ->(v) { {v => {key => value}} }]
|
324
|
-
end
|
325
|
-
end
|
326
|
-
elsif arg.is_a?(String)
|
327
|
-
result << [:where, arg]
|
329
|
+
new_link.tap do |new_query|
|
330
|
+
Link.for_args(@model, method, args).each { |link| new_query._add_links(link) }
|
328
331
|
end
|
329
|
-
result
|
330
|
-
end
|
331
|
-
alias_method :links_for_node_where_arg, :links_for_where_arg
|
332
|
-
|
333
|
-
def links_for_association(name, value, n_string)
|
334
|
-
neo_id = value.try(:neo_id) || value
|
335
|
-
fail ArgumentError, "Invalid value for '#{name}' condition" if not neo_id.is_a?(Integer)
|
336
|
-
|
337
|
-
dir = @model.associations[name].direction
|
338
|
-
|
339
|
-
arrow = dir == :out ? '-->' : '<--'
|
340
|
-
[
|
341
|
-
[:match, ->(v) { "#{v}#{arrow}(#{n_string})" }],
|
342
|
-
[:where, ->(_) { {"ID(#{n_string})" => neo_id.to_i} }]
|
343
|
-
]
|
344
|
-
end
|
345
|
-
|
346
|
-
# We don't accept strings here. If you want to use a string, just use where.
|
347
|
-
def links_for_rel_where_arg(arg)
|
348
|
-
arg.each_with_object([]) do |(key, value), result|
|
349
|
-
result << [:where, ->(_) { {rel_var => {key => value}} }]
|
350
|
-
end
|
351
|
-
end
|
352
|
-
|
353
|
-
def links_for_order_arg(arg)
|
354
|
-
[[:order, ->(v) { arg.is_a?(String) ? arg : {v => arg} }]]
|
355
|
-
end
|
356
|
-
|
357
|
-
def match_string(node)
|
358
|
-
":`#{node.class.mapped_label_name}`" if node.class.respond_to?(:mapped_label_name)
|
359
332
|
end
|
360
333
|
end
|
361
334
|
end
|