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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +584 -0
  3. data/CONTRIBUTORS +7 -28
  4. data/Gemfile +6 -1
  5. data/README.md +54 -8
  6. data/lib/neo4j.rb +5 -0
  7. data/lib/neo4j/active_node.rb +1 -0
  8. data/lib/neo4j/active_node/dependent/association_methods.rb +35 -17
  9. data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +21 -19
  10. data/lib/neo4j/active_node/has_n.rb +377 -132
  11. data/lib/neo4j/active_node/has_n/association.rb +77 -38
  12. data/lib/neo4j/active_node/id_property.rb +46 -28
  13. data/lib/neo4j/active_node/initialize.rb +18 -6
  14. data/lib/neo4j/active_node/labels.rb +69 -35
  15. data/lib/neo4j/active_node/node_wrapper.rb +37 -30
  16. data/lib/neo4j/active_node/orm_adapter.rb +5 -4
  17. data/lib/neo4j/active_node/persistence.rb +53 -10
  18. data/lib/neo4j/active_node/property.rb +13 -5
  19. data/lib/neo4j/active_node/query.rb +11 -10
  20. data/lib/neo4j/active_node/query/query_proxy.rb +126 -153
  21. data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +15 -25
  22. data/lib/neo4j/active_node/query/query_proxy_link.rb +89 -0
  23. data/lib/neo4j/active_node/query/query_proxy_methods.rb +72 -19
  24. data/lib/neo4j/active_node/query_methods.rb +3 -1
  25. data/lib/neo4j/active_node/scope.rb +17 -21
  26. data/lib/neo4j/active_node/validations.rb +8 -2
  27. data/lib/neo4j/active_rel/initialize.rb +1 -2
  28. data/lib/neo4j/active_rel/persistence.rb +21 -33
  29. data/lib/neo4j/active_rel/property.rb +4 -2
  30. data/lib/neo4j/active_rel/types.rb +20 -8
  31. data/lib/neo4j/config.rb +16 -6
  32. data/lib/neo4j/core/query.rb +2 -2
  33. data/lib/neo4j/errors.rb +10 -0
  34. data/lib/neo4j/migration.rb +57 -46
  35. data/lib/neo4j/paginated.rb +3 -1
  36. data/lib/neo4j/railtie.rb +26 -14
  37. data/lib/neo4j/shared.rb +7 -1
  38. data/lib/neo4j/shared/declared_property.rb +62 -0
  39. data/lib/neo4j/shared/declared_property_manager.rb +150 -0
  40. data/lib/neo4j/shared/persistence.rb +15 -8
  41. data/lib/neo4j/shared/property.rb +64 -49
  42. data/lib/neo4j/shared/rel_type_converters.rb +13 -12
  43. data/lib/neo4j/shared/serialized_properties.rb +0 -15
  44. data/lib/neo4j/shared/type_converters.rb +53 -47
  45. data/lib/neo4j/shared/typecaster.rb +49 -0
  46. data/lib/neo4j/version.rb +1 -1
  47. data/lib/rails/generators/neo4j/model/model_generator.rb +3 -3
  48. data/lib/rails/generators/neo4j_generator.rb +5 -12
  49. data/neo4j.gemspec +4 -3
  50. metadata +30 -11
  51. 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
- self.props.symbolize_keys!
7
- most_concrete_class = sorted_wrapper_classes
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
- def checked_labels_set
15
- @@_checked_labels_set ||= Set.new
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 check_label(label_name)
19
- unless checked_labels_set.include?(label_name)
20
- load_class_from_label(label_name)
21
- # do this only once
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
- # Makes the determination of whether to use <tt>_classname</tt> (or whatever is defined by config) or the node's labels.
27
- def sorted_wrapper_classes
28
- if self.props.is_a?(Hash) && self.props.key?(Neo4j::Config.class_name_property)
29
- self.props[Neo4j::Config.class_name_property].constantize
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
- def load_class_from_label(label_name)
39
- label_name.to_s.split('::').inject(Kernel) { |container, name| container.const_get(name.to_s) }
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 _class_wrappers
45
- labels.find_all do |label_name|
46
- check_label(label_name)
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.find(wrap_key(id))
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
- if id = conditions.delete(:id)
76
- conditions[klass.id_property_name.to_sym] = id
77
- end
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. If the flag is false validations are bypassed altogether. See ActiveRecord::Validations for more information.
20
- # There's a series of callbacks associated with save. If any of the before_* callbacks return false the action is cancelled and save returns false.
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
- clear_association_cache
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
- private
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 persisted? && !@relationship_props.nil?
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 relationships of the model
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.keys.each_with_object({}) do |key, association_props|
17
- association_props[key] = attributes.delete(key) if self.association?(key)
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 caller set to the node instance.
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, caller: self).match_to(self)
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 var [Symbol, String] The variable name to specify in the query
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
- module_eval(%{
50
- def #{method}(*args)
51
- self.query_proxy.#{method}(*args)
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 :caller, :association, :model, :starting_query
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
- # * caller: The node instance at the start of the QueryProxy chain
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
- @node_var = options[:node]
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
- @session = options[:session]
43
- @caller = options[:caller]
44
- @chain_level = options[:chain_level]
47
+
45
48
  @chain = []
46
- @starting_query = options[:starting_query]
47
- @optional = options[:optional]
48
- @params = options[:query_proxy] ? options[:query_proxy].instance_variable_get('@params') : {}
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
- self.dup.tap do |new_query|
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
- base_query = if @association
84
- chain_var = _association_chain_var
85
- label_string = @model && ":`#{@model.mapped_label_name}`"
86
- (_association_query_start(chain_var) & _query_model_as(var)).send(_match_type, "#{chain_var}#{_association_arrow}(#{var}#{label_string})")
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, @model.current_scope = @model.current_scope, self
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
- module_eval(%{
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 associations on associations' unless @association
155
- other_nodes = [other_nodes].flatten
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
- other_nodes = other_nodes.map do |other_node|
158
- case other_node
159
- when Integer, String
160
- @model.find(other_node)
161
- else
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.compact
185
+ end
186
+ end
165
187
 
166
- fail ArgumentError, "Node must be of the association's class when model is specified" if @model && other_nodes.any? { |other_node| !other_node.is_a?(@model) }
167
- other_nodes.each do |other_node|
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
- return false if @association.perform_callback(@options[:start_object], other_node, :before) == false
191
+ pluck(@rel_var)
192
+ end
172
193
 
173
- start_object = @options[:start_object]
174
- start_object.clear_association_cache
175
- _session.query(context: @options[:context])
176
- .match("(start#{match_string(start_object)}), (end#{match_string(other_node)})").where('ID(start) = {start_id} AND ID(end) = {end_id}')
177
- .params(start_id: start_object.neo_id, end_id: other_node.neo_id)
178
- .send(create_method, "start#{_association_arrow(properties, true)}end").exec
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
- @association.perform_callback(@options[:start_object], other_node, :after)
181
- # end
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
- match_arg = if @model
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
- # TODO: Refactor this. Too much happening here.
229
- def _result_string
230
- if self.association
231
- "result_#{self.association.name}".to_sym
232
- elsif self.model && self.model.name
233
- label = "result_#{self.model.name}"
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
- :result
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
- if query_proxy = @options[:query_proxy]
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 = @options[:start_object]
303
+ if start_object
261
304
  :"#{start_object.class.name.gsub('::', '_').downcase}#{start_object.neo_id}"
262
- elsif query_proxy = @options[:query_proxy]
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 start_object = @options[:start_object]
271
- start_object.query_as(var)
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
- self.dup.tap do |new_query|
297
- args.each do |arg|
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