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,29 +1,8 @@
1
- Maintainer:
2
- Andreas Ronge <andreas dot ronge at gmail dot com>
1
+ Maintainers:
2
+ Chris Grigg <subvertallchris @ GitHub>
3
+ Brian Underwood <cheerfulstoic @ GitHub>
3
4
 
4
- Contributors:
5
- * Dmytrii Nagirniak
6
- * Marcio Toshio
7
- * Kalyan Akella
8
- * Vivek Prahlad
9
- * Deepak N
10
- * Frédéric Vanclef
11
- * Pere Urbon
12
- * Joe Leaver
13
- * Junegunn Choi
14
- * Jay Adkisson
15
- * Stephan Hagemann
16
- * Bobby Calderwood
17
- * Ben Jackson
18
- * Dwight van Tuyl
19
- * Martin Kleppmann
20
- * Peter Neubauer
21
- * Jan-Felix Wittmann
22
- * Marius Mårnes Mathiesen
23
- * Bert Fitié
24
- * Jan Berkel
25
- * David Beckwith
26
- * Johny Ho
27
- * Carlo Cabanilla
28
- * Anders Janmyr
29
- * Nick Sieger
5
+ Creator:
6
+ Andreas Ronge <andreasronge @ GitHub>
7
+
8
+ See: https://github.com/neo4jrb/neo4j/graphs/contributors
data/Gemfile CHANGED
@@ -2,14 +2,19 @@ source 'http://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'neo4j-core', github: 'neo4jrb/neo4j-core', branch: 'master'
5
+ # gem 'neo4j-core', github: 'neo4jrb/neo4j-core', branch: 'master'
6
6
  # gem 'neo4j-core', path: '../neo4j-core'
7
7
 
8
+ # gem 'active_attr', github: 'neo4jrb/active_attr', branch: 'performance'
9
+ # gem 'active_attr', path: '../active_attr'
10
+
8
11
  group 'test' do
9
12
  gem 'coveralls', require: false
13
+ gem 'codecov', require: false
10
14
  gem 'simplecov', require: false
11
15
  gem 'simplecov-html', require: false
12
16
  gem 'rspec', '~> 2.0'
13
17
  gem 'its'
14
18
  gem 'test-unit'
19
+ gem 'overcommit'
15
20
  end
data/README.md CHANGED
@@ -1,8 +1,36 @@
1
1
  # Welcome to Neo4j.rb
2
+
3
+ ## Code Status
4
+
2
5
  [![Build Status](https://secure.travis-ci.org/neo4jrb/neo4j.png?branch=master)](http://travis-ci.org/neo4jrb/neo4j) [![Coverage Status](https://coveralls.io/repos/neo4jrb/neo4j/badge.png?branch=master)](https://coveralls.io/r/neo4jrb/neo4j?branch=master) [![Code Climate](https://codeclimate.com/github/neo4jrb/neo4j.png)](https://codeclimate.com/github/neo4jrb/neo4j) [![PullReview stats](https://www.pullreview.com/github/neo4jrb/neo4j/badges/master.svg?)](https://www.pullreview.com/github/neo4jrb/neo4j/reviews/master)
3
6
 
7
+ ## Issues
8
+
9
+ [![Next Release](https://badge.waffle.io/neo4jrb/neo4j.png?label=Next%20Release&title=Next%20Release) ![In Progress](https://badge.waffle.io/neo4jrb/neo4j.png?label=In%20Progress&title=In%20Progress) ![In Master](https://badge.waffle.io/neo4jrb/neo4j.png?label=In%20Master&title=In%20Master)](https://waffle.io/neo4jrb/neo4j)
10
+
11
+ [![Post an issue](https://img.shields.io/badge/Bug%3F-Post%20an%20issue!-blue.svg)](https://waffle.io/neo4jrb/neo4j)
12
+
13
+ ## Get Support
14
+
15
+ [![StackOverflow](https://img.shields.io/badge/StackOverflow-Ask%20a%20question!-blue.svg)](http://stackoverflow.com/questions/ask?tags=neo4j.rb+neo4j+ruby) [![Gitter](https://img.shields.io/badge/Gitter-Join%20our%20chat!-blue.svg)](https://gitter.im/neo4jrb/neo4j?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Twitter](https://img.shields.io/badge/Twitter-Tweet%20with%20us!-blue.svg)](https://twitter.com/neo4jrb) [![Mailing list](https://img.shields.io/badge/Mailing%20list-Mail%20us!-blue.svg)](https://groups.google.com/forum/#!forum/neo4jrb)
16
+
17
+ # Introduction
18
+
4
19
  Neo4j.rb is an Active Model compliant Ruby/JRuby wrapper for [the Neo4j graph database](http://www.neo4j.org/). It uses the [neo4j-core](https://github.com/neo4jrb/neo4j-core) and [active_attr](https://github.com/cgriego/active_attr) gems.
5
20
 
21
+ Neo4j is a transactional, open-source graph database. A graph database manages data in a connected data structure, capable of representing any kind of data in a very accessible way. Information is stored in nodes and relationships connecting them, both of which can have arbitrary properties. To learn more visit [What is a Graph Database?](http://neo4j.com/developer/graph-database/)
22
+
23
+ With this gem you not only do you get a convenient higher level wrapper around Neo4j, but you have access to a powerful high-level query building interface which lets you take advantage of the power of Neo4j like this:
24
+
25
+ ```ruby
26
+ # Break down the top countries where friends' favorite beers come from
27
+ person.friends.favorite_beers.country_of_origin(:country).
28
+ order('count(country) DESC').
29
+ pluck(:country, count: 'count(country)')
30
+ ```
31
+
32
+ It can be installed in your `Gemfile` with a simple `gem 'neo4j'`
33
+
6
34
  For a general overview see our website: http://neo4jrb.io/
7
35
 
8
36
  Winner of a 2014 Graphie for "Best Community Contribution" at Neo4j's [Graph Connect](http://graphconnect.com) conference!
@@ -10,6 +38,25 @@ Winner of a 2014 Graphie for "Best Community Contribution" at Neo4j's [Graph Con
10
38
 
11
39
  Neo4j.rb v4.1.0 was released in January of 2015. Its changes are outlined [here](https://github.com/neo4jrb/neo4j/wiki/Neo4j.rb-v4-Introduction) and in the [announcement message](http://neo4jrb.io/blog/2015/01/09/neo4j-rb_v4-1_released.html). It will take a little time before all documentation is updated to reflect the new release but unless otherwise noted, all 3.X documentation is totally valid for v4.
12
40
 
41
+ ## Neo4j version support
42
+
43
+ | **Neo4j Version** | v2.x | v3.x | v4.x |
44
+ |-------------------|------|------|------|
45
+ | 1.9.x | Yes | No | No |
46
+ | 2.0.x | No | Yes | No |
47
+ | 2.1.x | No | Yes | Yes |
48
+ | 2.2.x | No | No | Yes |
49
+
50
+ ## Neo4j feature support
51
+
52
+ | **Neo4j Feature** | v2.x | v3.x | v4.x |
53
+ |----------------------------|--------|------|------|
54
+ | Auth | No | No | Yes |
55
+ | Remote Cypher | Yes | Yes | Yes |
56
+ | Transactions | Yes | Yes | Yes |
57
+ | High Availability | No | Yes | Yes |
58
+ | Embedded JVM support | Yes | Yes | Yes |
59
+
13
60
  ## Modern (3.x/4.X) Documentation
14
61
 
15
62
  * [Website](http://neo4jrb.io/) (for an introduction)
@@ -20,23 +67,22 @@ Neo4j.rb v4.1.0 was released in January of 2015. Its changes are outlined [here]
20
67
  * [README](https://github.com/neo4jrb/neo4j/tree/2.x)
21
68
  * [Wiki](https://github.com/neo4jrb/neo4j/wiki/Neo4j%3A%3ARails-Introduction)
22
69
 
23
- ## Support
24
-
25
- * Open an [issue](https://github.com/neo4jrb/neo4j/issues), post to [Stack Overflow](http://stackoverflow.com/questions/tagged/neo4j+ruby), or contact one of the developers.
26
- * [Neo4j.rb mailing list](https://groups.google.com/forum/#!forum/neo4jrb)
27
- * Consulting support? Ask any of the developers, info below.
28
-
29
70
  ## Developers
30
71
 
72
+ ### Original Author
73
+
31
74
  * [Andreas Ronge](https://github.com/andreasronge)
75
+
76
+ ### Current Maintainers
77
+
32
78
  * [Brian Underwood](https://github.com/cheerfulstoic)
33
79
  * [Chris Grigg](https://github.com/subvertallchris)
34
80
 
81
+ * Consulting support? Contact [Chris](http://subvertallmedia.com/) and/or [Brian](http://www.brian-underwood.codes/)
35
82
 
36
83
  ## Contributing
37
84
 
38
- Pull request with high test coverage and good [code climate](https://codeclimate.com/github/neo4jrb/neo4j) values will be accepted faster.
39
-
85
+ Always welcome! Please review the [guidelines for contributing](CONTRIBUTING.md) to this repository.
40
86
 
41
87
  ## License
42
88
 
@@ -16,6 +16,7 @@ require 'active_support/concern'
16
16
  require 'active_support/core_ext/class/attribute.rb'
17
17
 
18
18
  require 'active_attr'
19
+ require 'neo4j/errors'
19
20
  require 'neo4j/config'
20
21
  require 'neo4j/wrapper'
21
22
  require 'neo4j/active_rel/rel_wrapper'
@@ -26,11 +27,14 @@ require 'neo4j/type_converters'
26
27
  require 'neo4j/paginated'
27
28
 
28
29
  require 'neo4j/shared/callbacks'
30
+ require 'neo4j/shared/declared_property'
31
+ require 'neo4j/shared/declared_property_manager'
29
32
  require 'neo4j/shared/property'
30
33
  require 'neo4j/shared/persistence'
31
34
  require 'neo4j/shared/validations'
32
35
  require 'neo4j/shared/identity'
33
36
  require 'neo4j/shared/serialized_properties'
37
+ require 'neo4j/shared/typecaster'
34
38
  require 'neo4j/shared'
35
39
 
36
40
  require 'neo4j/active_rel/callbacks'
@@ -50,6 +54,7 @@ require 'neo4j/active_node/query_methods'
50
54
  require 'neo4j/active_node/query/query_proxy_methods'
51
55
  require 'neo4j/active_node/query/query_proxy_enumerable'
52
56
  require 'neo4j/active_node/query/query_proxy_find_in_batches'
57
+ require 'neo4j/active_node/query/query_proxy_link'
53
58
  require 'neo4j/active_node/labels'
54
59
  require 'neo4j/active_node/id_property'
55
60
  require 'neo4j/active_node/callbacks'
@@ -51,6 +51,7 @@ module Neo4j
51
51
  attributes.each_pair { |k, v| other.attributes[k] = v }
52
52
  inherit_serialized_properties(other) if self.respond_to?(:serialized_properties)
53
53
  Neo4j::ActiveNode::Labels.add_wrapped_class(other)
54
+
54
55
  super
55
56
  end
56
57
 
@@ -2,26 +2,44 @@ module Neo4j
2
2
  module ActiveNode
3
3
  module Dependent
4
4
  module AssociationMethods
5
+ def validate_dependent(value)
6
+ fail ArgumentError, "Invalid dependent value: #{value.inspect}" if not valid_dependent_value?(value)
7
+ end
8
+
5
9
  def add_destroy_callbacks(model)
6
10
  return if dependent.nil?
7
- # Bound value for procs
8
- assoc = self
9
-
10
- fn = case dependent
11
- when :delete
12
- proc { |o| o.send("#{assoc.name}_query_proxy").delete_all }
13
- when :delete_orphans
14
- proc { |o| o.as(:self).unique_nodes(assoc, :self, :n, :other_rel).query.delete(:n, :other_rel).exec }
15
- when :destroy
16
- proc { |o| o.send("#{assoc.name}_query_proxy").each_for_destruction(o) { |node| node.destroy } }
17
- when :destroy_orphans
18
- proc { |o| o.as(:self).unique_nodes(assoc, :self, :n, :other_rel).each_for_destruction(o) { |node| node.destroy } }
19
- else
20
- fail "Unknown dependent option #{dependent}"
21
- end
22
-
23
- model.before_destroy fn
11
+
12
+ model.before_destroy(&method("dependent_#{dependent}_callback"))
13
+ rescue NameError
14
+ raise "Unknown dependent option #{dependent}"
15
+ end
16
+
17
+ private
18
+
19
+ def valid_dependent_value?(value)
20
+ return true if value.nil?
21
+
22
+ self.respond_to?("dependent_#{value}_callback", true)
23
+ end
24
+
25
+ # Callback methods
26
+ def dependent_delete_callback(object)
27
+ object.association_query_proxy(name).delete_all
28
+ end
29
+
30
+ def dependent_delete_orphans_callback(object)
31
+ object.as(:self).unique_nodes(self, :self, :n, :other_rel).query.delete(:n, :other_rel).exec
32
+ end
33
+
34
+ def dependent_destroy_callback(object)
35
+ object.association_query_proxy(name).each_for_destruction(object, &:destroy)
36
+ end
37
+
38
+ def dependent_destroy_orphans_callback(object)
39
+ object.as(:self).unique_nodes(self, :self, :n, :other_rel).each_for_destruction(object, &:destroy)
24
40
  end
41
+
42
+ # End callback methods
25
43
  end
26
44
  end
27
45
  end
@@ -5,13 +5,15 @@ module Neo4j
5
5
  module QueryProxyMethods
6
6
  # Used as part of `dependent: :destroy` and may not have any utility otherwise.
7
7
  # It keeps track of the node responsible for a cascading `destroy` process.
8
- # @param [#dependent_children] caller The node that called this method. Typically, we would use QueryProxy's `caller` method
8
+ # @param [#dependent_children] source_object The node that called this method. Typically, we would use QueryProxy's `source_object` method
9
9
  # but this is not always available, so we require it explicitly.
10
10
  def each_for_destruction(owning_node)
11
11
  target = owning_node.called_by || owning_node
12
- enumerable_query(identity).each do |obj|
13
- # Cypher can return nil objects, check for empty results
14
- next if !obj || target.dependent_children.include?(obj)
12
+ objects = pluck(identity).compact.reject do |obj|
13
+ target.dependent_children.include?(obj)
14
+ end
15
+
16
+ objects.each do |obj|
15
17
  obj.called_by = target
16
18
  target.dependent_children << obj
17
19
  yield obj
@@ -25,22 +27,22 @@ module Neo4j
25
27
  # @param [String, Symbol] other_rel The identifier to use for the relationship in the optional match.
26
28
  # @return [Neo4j::ActiveNode::Query::QueryProxy]
27
29
  def unique_nodes(association, self_identifer, other_node, other_rel)
28
- fail 'Only supported by in QueryProxy chains started by an instance' unless caller
29
- both_string = "-[:`#{association.relationship_type}`]-"
30
- in_string = "<#{both_string}"
31
- out_string = "#{both_string}>"
32
- primary_rel, inverse_rel = case association.direction
33
- when :out
34
- [out_string, in_string]
35
- when :in
36
- [in_string, out_string]
37
- else
38
- [both_string, both_string]
39
- end
30
+ fail 'Only supported by in QueryProxy chains started by an instance' unless source_object
31
+
32
+ unique_nodes_query(association, self_identifer, other_node, other_rel)
33
+ .proxy_as(association.target_class, other_node)
34
+ end
35
+
36
+ private
40
37
 
41
- query.with(identity).proxy_as_optional(caller.class, self_identifer)
42
- .send("#{association.name}", other_node, other_rel)
43
- .where("NOT EXISTS((#{self_identifer})#{primary_rel}(#{other_node})#{inverse_rel}())")
38
+ def unique_nodes_query(association, self_identifer, other_node, other_rel)
39
+ query.with(identity).proxy_as_optional(source_object.class, self_identifer)
40
+ .send(association.name, other_node, other_rel)
41
+ .query
42
+ .with(other_node)
43
+ .match("()#{association.arrow_cypher}(#{other_node})")
44
+ .with(other_node, count: 'count(*)')
45
+ .where('count = 1')
44
46
  end
45
47
  end
46
48
  end
@@ -4,54 +4,138 @@ module Neo4j::ActiveNode
4
4
 
5
5
  class NonPersistedNodeError < StandardError; end
6
6
 
7
- # Clears out the association cache.
8
- def clear_association_cache #:nodoc:
9
- association_cache.clear if _persisted_obj
10
- end
7
+ # Return this object from associations
8
+ # It uses a QueryProxy to get results
9
+ # But also caches results and can have results cached on it
10
+ class AssociationProxy
11
+ def initialize(query_proxy, cached_result = nil)
12
+ @query_proxy = query_proxy
13
+ cache_result(cached_result)
11
14
 
12
- # Returns the current association cache. It is in the format
13
- # { :association_name => { :hash_of_cypher_string => [collection] }}
14
- def association_cache
15
- @association_cache ||= {}
16
- end
15
+ # Represents the thing which can be enumerated
16
+ # default to @query_proxy, but will be set to
17
+ # @cached_result if that is set
18
+ @enumerable = @query_proxy
19
+ end
17
20
 
18
- # Returns the specified association instance if it responds to :loaded?, nil otherwise.
19
- # @param [String] cypher_string the cypher, with params, used for lookup
20
- # @param [Enumerable] association_obj the HasN::Association object used to perform this query
21
- def association_instance_get(cypher_string, association_obj)
22
- return if association_cache.nil? || association_cache.empty?
23
- lookup_obj = cypher_hash(cypher_string)
24
- reflection = association_reflection(association_obj)
25
- return if reflection.nil?
26
- association_cache[reflection.name] ? association_cache[reflection.name][lookup_obj] : nil
27
- end
21
+ # States:
22
+ # Default
23
+ def inspect
24
+ if @cached_result
25
+ @cached_result.inspect
26
+ else
27
+ "<AssociationProxy @query_proxy=#{@query_proxy.inspect}>"
28
+ end
29
+ end
30
+
31
+ extend Forwardable
32
+ %w(include? empty? count find ==).each do |delegated_method|
33
+ def_delegator :@enumerable, delegated_method
34
+ end
35
+
36
+ include Enumerable
37
+
38
+ def each(&block)
39
+ result.each(&block)
40
+ end
41
+
42
+ def result
43
+ return @cached_result if @cached_result
44
+
45
+ cache_query_proxy_result
46
+
47
+ @cached_result
48
+ end
49
+
50
+ def cache_result(result)
51
+ @cached_result = result
52
+ @enumerable = (@cached_result || @query_proxy)
53
+ end
54
+
55
+ def cache_query_proxy_result
56
+ @query_proxy.to_a.tap do |result|
57
+ result.each do |object|
58
+ object.instance_variable_set('@association_proxy', self)
59
+ end
60
+ cache_result(result)
61
+ end
62
+ end
63
+
64
+ def clear_cache_result
65
+ cache_result(nil)
66
+ end
67
+
68
+ def cached?
69
+ !!@cached_result
70
+ end
71
+
72
+ QUERY_PROXY_METHODS = [:<<, :delete]
73
+ CACHED_RESULT_METHODS = []
74
+
75
+ def method_missing(method_name, *args, &block)
76
+ target = target_for_missing_method(method_name)
77
+
78
+ cache_query_proxy_result if !cached? && !target.is_a?(Neo4j::ActiveNode::Query::QueryProxy)
79
+
80
+ clear_cache_result if target.is_a?(Neo4j::ActiveNode::Query::QueryProxy)
81
+
82
+ return if target.nil?
83
+
84
+ target.public_send(method_name, *args, &block)
85
+ end
86
+
87
+ def with_associations(*spec)
88
+ return_object_clause = '[' + spec.map { |n| "collect(#{n})" }.join(',') + ']'
89
+ query_from_association_spec(spec).pluck(:previous, return_object_clause).map do |record, eager_data|
90
+ eager_data.each_with_index do |eager_records, index|
91
+ record.send(spec[index]).cache_result(eager_records)
92
+ end
93
+
94
+ record
95
+ end
96
+ end
97
+
98
+ private
28
99
 
29
- # @return [Hash] A hash of all queries in @association_cache created from the association owning this reflection
30
- def association_instance_get_by_reflection(reflection_name)
31
- association_cache[reflection_name]
100
+ def query_from_association_spec(spec)
101
+ spec.inject(@query_proxy.query_as(:previous).return(:previous)) do |query, association_name|
102
+ association = @query_proxy.model.associations[association_name]
103
+ query.optional_match("previous#{association.arrow_cypher}#{association_name}")
104
+ end
105
+ end
106
+
107
+ def target_for_missing_method(method_name)
108
+ case method_name
109
+ when *QUERY_PROXY_METHODS
110
+ @query_proxy
111
+ when *CACHED_RESULT_METHODS
112
+ @cached_result
113
+ else
114
+ if @cached_result && @cached_result.respond_to?(method_name)
115
+ @cached_result
116
+ elsif @query_proxy.respond_to?(method_name)
117
+ @query_proxy
118
+ end
119
+ end
120
+ end
32
121
  end
33
122
 
34
- # Caches an association result. Unlike ActiveRecord, which stores results in @association_cache using { :association_name => [collection_result] },
35
- # ActiveNode stores it using { :association_name => { :hash_string_of_cypher => [collection_result] }}.
36
- # This is necessary because an association name by itself does not take into account :where, :limit, :order, etc,... so it's prone to error.
37
- # @param [Neo4j::ActiveNode::Query::QueryProxy] query_proxy The QueryProxy object that resulted in this result
38
- # @param [Enumerable] collection_result The result of the query after calling :each
39
- # @param [Neo4j::ActiveNode::HasN::Association] association_obj The association traversed to create the result
40
- def association_instance_set(cypher_string, collection_result, association_obj)
41
- return collection_result if Neo4j::Transaction.current
42
- cache_key = cypher_hash(cypher_string)
43
- reflection = association_reflection(association_obj)
44
- return if reflection.nil?
45
- if @association_cache[reflection.name]
46
- @association_cache[reflection.name][cache_key] = collection_result
47
- else
48
- @association_cache[reflection.name] = {cache_key => collection_result}
49
- end
50
- collection_result
123
+ # Returns the current AssociationProxy cache for the association cache. It is in the format
124
+ # { :association_name => AssociationProxy}
125
+ # This is so that we
126
+ # * don't need to re-build the QueryProxy objects
127
+ # * also because the QueryProxy object caches it's results
128
+ # * so we don't need to query again
129
+ # * so that we can cache results from association calls or eager loading
130
+ def association_proxy_cache
131
+ @association_proxy_cache ||= {}
51
132
  end
52
133
 
53
- def association_reflection(association_obj)
54
- self.class.reflect_on_association(association_obj.name)
134
+ def association_proxy_cache_fetch(key)
135
+ association_proxy_cache.fetch(key) do
136
+ value = yield
137
+ association_proxy_cache[key] = value
138
+ end
55
139
  end
56
140
 
57
141
  # Uses the cypher generated by a QueryProxy object, complete with params, to generate a basic non-cryptographic hash
@@ -62,6 +146,54 @@ module Neo4j::ActiveNode
62
146
  cypher_string.hash.abs
63
147
  end
64
148
 
149
+ def association_query_proxy(name, options = {})
150
+ self.class.send(:association_query_proxy, name, {start_object: self}.merge(options))
151
+ end
152
+
153
+ def association_proxy(name, options = {})
154
+ name = name.to_sym
155
+ hash = [name, options.values_at(:node, :rel)].hash
156
+
157
+ association_proxy_cache_fetch(hash) do
158
+ if previous_association_proxy = self.instance_variable_get('@association_proxy')
159
+ result_by_previous_id = previous_association_proxy_results_by_previous_id(previous_association_proxy, name)
160
+
161
+ previous_association_proxy.result.inject(nil) do |proxy_to_return, object|
162
+ proxy = fresh_association_proxy(name, options, result_by_previous_id[object.neo_id])
163
+
164
+ object.association_proxy_cache[hash] = proxy
165
+
166
+ (self == object ? proxy : proxy_to_return)
167
+ end
168
+ else
169
+ fresh_association_proxy(name, options)
170
+ end
171
+ end
172
+ end
173
+
174
+ private
175
+
176
+ def fresh_association_proxy(name, options = {}, cached_result = nil)
177
+ AssociationProxy.new(association_query_proxy(name, options), cached_result)
178
+ end
179
+
180
+ def previous_association_proxy_results_by_previous_id(association_proxy, association_name)
181
+ query_proxy = self.class.as(:previous).where(neo_id: association_proxy.result.map(&:neo_id))
182
+ query_proxy = self.class.send(:association_query_proxy, association_name, previous_query_proxy: query_proxy, node: :next)
183
+
184
+ Hash[*query_proxy.pluck('ID(previous)', 'collect(next)').flatten(1)]
185
+ end
186
+
187
+ def handle_non_persisted_node(other_node)
188
+ return unless Neo4j::Config[:autosave_on_assignment]
189
+ other_node.try(:save)
190
+ save
191
+ end
192
+
193
+ def validate_persisted_for_association!
194
+ fail(Neo4j::ActiveNode::HasN::NonPersistedNodeError, 'Unable to create relationship with non-persisted nodes') unless self._persisted_obj
195
+ end
196
+
65
197
  module ClassMethods
66
198
  # :nocov:
67
199
  # rubocop:disable Style/PredicateName
@@ -78,119 +210,232 @@ module Neo4j::ActiveNode
78
210
  end
79
211
 
80
212
  def associations
81
- @associations || {}
213
+ @associations ||= {}
214
+ end
215
+
216
+ def associations_keys
217
+ @associations_keys ||= associations.keys
82
218
  end
83
219
 
84
220
  # make sure the inherited classes inherit the <tt>_decl_rels</tt> hash
85
221
  def inherited(klass)
86
222
  klass.instance_variable_set(:@associations, associations.clone)
223
+ @associations_keys = klass.associations_keys.clone
87
224
  super
88
225
  end
89
226
 
90
- # rubocop:disable Style/PredicateName
91
- def has_many(direction, name, options = {})
227
+ # For defining an "has many" association on a model. This defines a set of methods on
228
+ # your model instances. For instance, if you define the association on a Person model:
229
+ #
230
+ # has_many :out, :vehicles, type: :has_vehicle
231
+ #
232
+ # This would define the following methods:
233
+ #
234
+ # **#vehicles**
235
+ # Returns a QueryProxy object. This is an Enumerable object and thus can be iterated
236
+ # over. It also has the ability to accept class-level methods from the Vehicle model
237
+ # (including calls to association methods)
238
+ #
239
+ # **#vehicles=**
240
+ # Takes an array of Vehicle objects and replaces all current ``:HAS_VEHICLE`` relationships
241
+ # with new relationships refering to the specified objects
242
+ #
243
+ # **.vehicles**
244
+ # Returns a QueryProxy object. This would represent all ``Vehicle`` objects associated with
245
+ # either all ``Person`` nodes (if ``Person.vehicles`` is called), or all ``Vehicle`` objects
246
+ # associated with the ``Person`` nodes thus far represented in the QueryProxy chain.
247
+ # For example:
248
+ # ``company.people.where(age: 40).vehicles``
249
+ #
250
+ # Arguments:
251
+ # **direction:**
252
+ # **Available values:** ``:in``, ``:out``, or ``:both``.
253
+ #
254
+ # Refers to the relative to the model on which the association is being defined.
255
+ #
256
+ # Example:
257
+ # ``Person.has_many :out, :posts, type: :wrote``
258
+ #
259
+ # means that a `WROTE` relationship goes from a `Person` node to a `Post` node
260
+ #
261
+ # **name:**
262
+ # The name of the association. The affects the methods which are created (see above).
263
+ # The name is also used to form default assumptions about the model which is being referred to
264
+ #
265
+ # Example:
266
+ # ``Person.has_many :out, :posts, type: :wrote``
267
+ #
268
+ # will assume a `model_class` option of ``'Post'`` unless otherwise specified
269
+ #
270
+ # **options:** A ``Hash`` of options. Allowed keys are:
271
+ # *type*: The Neo4j relationship type. This option is required unless either the
272
+ # `origin` or `rel_class` options are specified
273
+ #
274
+ # *origin*: The name of the association from another model which the `type` and `model_class`
275
+ # can be gathered.
276
+ #
277
+ # Example:
278
+ # ``Person.has_many :out, :posts, origin: :author`` (`model_class` of `Post` is assumed here)
279
+ #
280
+ # ``Post.has_one :in, :author, type: :has_author, model_class: 'Person'``
281
+ #
282
+ # *model_class*: The model class to which the association is referring. Can be either a
283
+ # model object ``include`` ing ``ActiveNode`` or a string (or an ``Array`` of same).
284
+ # **A string is recommended** to avoid load-time issues
285
+ #
286
+ # *rel_class*: The ``ActiveRel`` class to use for this association. Can be either a
287
+ # model object ``include`` ing ``ActiveRel`` or a string (or an ``Array`` of same).
288
+ # **A string is recommended** to avoid load-time issues
289
+ #
290
+ # *dependent*: Enables deletion cascading.
291
+ # **Available values:** ``:delete``, ``:delete_orphans``, ``:destroy``, ``:destroy_orphans``
292
+ # (note that the ``:destroy_orphans`` option is known to be "very metal". Caution advised)
293
+ #
294
+ def has_many(direction, name, options = {}) # rubocop:disable Style/PredicateName
295
+ validate_association_options!(name, options)
92
296
  name = name.to_sym
93
- association = build_association(:has_many, direction, name, options)
94
- # TODO: Make assignment more efficient? (don't delete nodes when they are being assigned)
95
- module_eval(%{
96
- def #{name}(node = nil, rel = nil)
97
- return [].freeze unless self._persisted_obj
98
- #{name}_query_proxy(node: node, rel: rel)
99
- end
297
+ build_association(:has_many, direction, name, options)
100
298
 
101
- def #{name}_query_proxy(options = {})
102
- Neo4j::ActiveNode::Query::QueryProxy.new(#{association.target_class_name_or_nil},
103
- self.class.associations[#{name.inspect}],
104
- {
105
- session: self.class.neo4j_session,
106
- start_object: self,
107
- node: options[:node],
108
- rel: options[:rel],
109
- optional: options[:optional],
110
- context: '#{self.name}##{name}',
111
- caller: self
112
- })
113
- end
299
+ define_has_many_methods(name)
300
+ end
114
301
 
115
- def #{name}=(other_nodes)
116
- #{name}(nil, :r).query_as(:n).delete(:r).exec
117
- clear_association_cache
118
- other_nodes.each { |node| #{name} << node }
119
- end
302
+ # For defining an "has one" association on a model. This defines a set of methods on
303
+ # your model instances. For instance, if you define the association on a Person model:
304
+ #
305
+ # has_one :out, :vehicle, type: :has_vehicle
306
+ #
307
+ # This would define the methods: ``#vehicle``, ``#vehicle=``, and ``.vehicle``.
308
+ #
309
+ # See :ref:`#has_many <Neo4j/ActiveNode/HasN/ClassMethods#has_many>` for anything
310
+ # not specified here
311
+ #
312
+ def has_one(direction, name, options = {}) # rubocop:disable Style/PredicateName
313
+ validate_association_options!(name, options)
314
+ name = name.to_sym
315
+ build_association(:has_one, direction, name, options)
120
316
 
121
- def #{name}_rels
122
- #{name}(nil, :r).pluck(:r)
123
- end}, __FILE__, __LINE__)
317
+ define_has_one_methods(name)
318
+ end
124
319
 
125
- instance_eval(%{
126
- def #{name}(node = nil, rel = nil, proxy_obj = nil, options = {})
127
- #{name}_query_proxy({node: node, rel: rel, proxy_obj: proxy_obj}.merge(options))
128
- end
320
+ private
129
321
 
130
- def #{name}_query_proxy(options = {})
131
- query_proxy = options[:proxy_obj] || Neo4j::ActiveNode::Query::QueryProxy.new(::#{self.name}, nil, {
132
- session: self.neo4j_session, query_proxy: nil, context: '#{self.name}' + '##{name}'
133
- })
134
- context = (query_proxy && query_proxy.context ? query_proxy.context : '#{self.name}') + '##{name}'
135
- Neo4j::ActiveNode::Query::QueryProxy.new(#{association.target_class_name_or_nil},
136
- associations[#{name.inspect}],
137
- {
138
- session: self.neo4j_session,
139
- query_proxy: query_proxy,
140
- node: options[:node],
141
- rel: options[:rel],
142
- context: context,
143
- optional: options[:optional] || query_proxy.optional?,
144
- caller: query_proxy.caller
145
- })
146
- end}, __FILE__, __LINE__)
147
- end
148
-
149
- def has_one(direction, name, options = {})
150
- name = name.to_sym
151
- association = build_association(:has_one, direction, name, options)
152
-
153
- module_eval(%{
154
- def #{name}=(other_node)
155
- raise(Neo4j::ActiveNode::HasN::NonPersistedNodeError, 'Unable to create relationship with non-persisted nodes') unless self._persisted_obj
156
- clear_association_cache
157
- #{name}_query_proxy(rel: :r).query_as(:n).delete(:r).exec
158
- #{name}_query_proxy << other_node
159
- end
322
+ VALID_ASSOCIATION_OPTION_KEYS = [:type, :origin, :model_class, :rel_class, :dependent, :before, :after]
160
323
 
161
- def #{name}_query_proxy(options = {})
162
- self.class.#{name}_query_proxy({start_object: self}.merge(options))
163
- end
324
+ def validate_association_options!(association_name, options)
325
+ type_keys = (options.keys & [:type, :origin, :rel_class])
326
+ message = case
327
+ when type_keys.size > 1
328
+ "Only one of 'type', 'origin', or 'rel_class' options are allowed for associations (#{self.class}##{association_name})"
329
+ when type_keys.empty?
330
+ "The 'type' option must be specified( even if it is `nil`) or `origin`/`rel_class` must be specified (#{self.class}##{association_name})"
331
+ when (unknown_keys = options.keys - VALID_ASSOCIATION_OPTION_KEYS).size > 0
332
+ "Unknown option(s) specified: #{unknown_keys.join(', ')} (#{self.class}##{association_name})"
333
+ end
164
334
 
165
- def #{name}_rel
166
- #{name}_query_proxy(rel: :r).pluck(:r).first
167
- end
335
+ fail ArgumentError, message if message
336
+ end
168
337
 
169
- def #{name}(node = nil, rel = nil)
170
- return nil unless self._persisted_obj
171
- result = #{name}_query_proxy(node: node, rel: rel, context: '#{self.name}##{name}')
172
- association = self.class.reflect_on_association(__method__)
173
- query_return = association_instance_get(result.to_cypher_with_params, association)
174
- query_return || association_instance_set(result.to_cypher_with_params, result.first, association)
175
- end}, __FILE__, __LINE__)
176
-
177
- instance_eval(%{
178
- def #{name}_query_proxy(options = {})
179
- Neo4j::ActiveNode::Query::QueryProxy.new(#{association.target_class_name_or_nil},
180
- associations[#{name.inspect}],
181
- {session: self.neo4j_session}.merge(options))
182
- end
338
+ def define_has_many_methods(name)
339
+ define_method(name) do |node = nil, rel = nil, options = {}|
340
+ return [].freeze unless self._persisted_obj
341
+
342
+ association_proxy(name, {node: node, rel: rel, source_object: self}.merge(options))
343
+ end
183
344
 
184
- def #{name}(node = nil, rel = nil, query_proxy = nil, options = {})
185
- context = (query_proxy && query_proxy.context ? query_proxy.context : '#{self.name}') + '##{name}'
186
- #{name}_query_proxy({query_proxy: query_proxy, node: node, rel: rel, context: context}.merge(options))
187
- end}, __FILE__, __LINE__)
345
+ define_has_many_setter(name)
346
+
347
+ define_class_method(name) do |node = nil, rel = nil, options = {}|
348
+ association_proxy(name, {node: node, rel: rel}.merge(options))
349
+ end
188
350
  end
189
- # rubocop:enable Style/PredicateName
190
351
 
191
- private
352
+ def define_has_many_setter(name)
353
+ define_method("#{name}=") do |other_nodes|
354
+ association_proxy_cache.clear
355
+
356
+ Neo4j::Transaction.run { association_proxy(name).replace_with(other_nodes) }
357
+ end
358
+ end
359
+
360
+ def define_has_one_methods(name)
361
+ define_method(name) do |node = nil, rel = nil|
362
+ return nil unless self._persisted_obj
363
+
364
+ association_proxy(name, node: node, rel: rel).first
365
+ end
366
+
367
+ define_has_one_setter(name)
368
+
369
+ define_class_method(name) do |node = nil, rel = nil, options = {}|
370
+ association_proxy(name, {node: node, rel: rel}.merge(options))
371
+ end
372
+ end
373
+
374
+ def define_has_one_setter(name)
375
+ define_method("#{name}=") do |other_node|
376
+ handle_non_persisted_node(other_node)
377
+ validate_persisted_for_association!
378
+ association_proxy_cache.clear # TODO: Should probably just clear for this association...
379
+
380
+ Neo4j::Transaction.run { association_proxy(name).replace_with(other_node) }
381
+ end
382
+ end
383
+
384
+ def define_class_method(*args, &block)
385
+ klass = class << self; self; end
386
+ klass.instance_eval do
387
+ define_method(*args, &block)
388
+ end
389
+ end
390
+
391
+ def association_query_proxy(name, options = {})
392
+ previous_query_proxy = options[:previous_query_proxy] || current_scope
393
+ query_proxy = previous_query_proxy || default_association_query_proxy(name)
394
+
395
+ Neo4j::ActiveNode::Query::QueryProxy.new(association_target_class(name),
396
+ associations[name],
397
+ {session: neo4j_session,
398
+ query_proxy: query_proxy,
399
+ context: "#{query_proxy.context || self.name}##{name}",
400
+ optional: query_proxy.optional?,
401
+ source_object: query_proxy.source_object}.merge(options)).tap do |query_proxy_result|
402
+ target_classes = association_target_classes(name)
403
+ return query_proxy_result.as_models(target_classes) if target_classes
404
+ end
405
+ end
406
+
407
+ def association_proxy(name, options = {})
408
+ query_proxy = association_query_proxy(name, options)
409
+
410
+ AssociationProxy.new(query_proxy)
411
+ end
412
+
413
+ def association_target_class(name)
414
+ target_classes_or_nil = associations[name].target_classes_or_nil
415
+
416
+ return if !target_classes_or_nil.is_a?(Array) || target_classes_or_nil.size != 1
417
+
418
+ target_classes_or_nil[0]
419
+ end
420
+
421
+ def association_target_classes(name)
422
+ target_classes_or_nil = associations[name].target_classes_or_nil
423
+
424
+ return if !target_classes_or_nil.is_a?(Array) || target_classes_or_nil.size <= 1
425
+
426
+ target_classes_or_nil
427
+ end
428
+
429
+ def default_association_query_proxy(name)
430
+ Neo4j::ActiveNode::Query::QueryProxy.new("::#{self.name}".constantize,
431
+ nil,
432
+ session: neo4j_session,
433
+ query_proxy: nil,
434
+ context: "#{self.name}##{name}")
435
+ end
192
436
 
193
437
  def build_association(macro, direction, name, options)
438
+ associations_keys << name
194
439
  Neo4j::ActiveNode::HasN::Association.new(macro, direction, name, options).tap do |association|
195
440
  @associations ||= {}
196
441
  @associations[name] = association