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.
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