neo4j 4.1.5 → 5.0.0.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|