activegraph 11.0.0.beta.1-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (144) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2016 -0
  3. data/CONTRIBUTORS +12 -0
  4. data/Gemfile +24 -0
  5. data/README.md +111 -0
  6. data/activegraph.gemspec +52 -0
  7. data/bin/rake +17 -0
  8. data/config/locales/en.yml +5 -0
  9. data/config/neo4j/add_classnames.yml +1 -0
  10. data/config/neo4j/config.yml +35 -0
  11. data/lib/active_graph.rb +123 -0
  12. data/lib/active_graph/ansi.rb +14 -0
  13. data/lib/active_graph/attribute_set.rb +32 -0
  14. data/lib/active_graph/base.rb +77 -0
  15. data/lib/active_graph/class_arguments.rb +39 -0
  16. data/lib/active_graph/config.rb +135 -0
  17. data/lib/active_graph/core.rb +14 -0
  18. data/lib/active_graph/core/connection_failed_error.rb +6 -0
  19. data/lib/active_graph/core/cypher_error.rb +37 -0
  20. data/lib/active_graph/core/entity.rb +11 -0
  21. data/lib/active_graph/core/instrumentable.rb +37 -0
  22. data/lib/active_graph/core/label.rb +135 -0
  23. data/lib/active_graph/core/logging.rb +44 -0
  24. data/lib/active_graph/core/node.rb +15 -0
  25. data/lib/active_graph/core/querable.rb +41 -0
  26. data/lib/active_graph/core/query.rb +485 -0
  27. data/lib/active_graph/core/query_builder.rb +18 -0
  28. data/lib/active_graph/core/query_clauses.rb +727 -0
  29. data/lib/active_graph/core/query_ext.rb +24 -0
  30. data/lib/active_graph/core/query_find_in_batches.rb +46 -0
  31. data/lib/active_graph/core/record.rb +51 -0
  32. data/lib/active_graph/core/result.rb +31 -0
  33. data/lib/active_graph/core/schema.rb +65 -0
  34. data/lib/active_graph/core/schema_errors.rb +12 -0
  35. data/lib/active_graph/core/wrappable.rb +30 -0
  36. data/lib/active_graph/errors.rb +59 -0
  37. data/lib/active_graph/lazy_attribute_hash.rb +38 -0
  38. data/lib/active_graph/migration.rb +148 -0
  39. data/lib/active_graph/migrations.rb +27 -0
  40. data/lib/active_graph/migrations/base.rb +77 -0
  41. data/lib/active_graph/migrations/check_pending.rb +20 -0
  42. data/lib/active_graph/migrations/helpers.rb +105 -0
  43. data/lib/active_graph/migrations/helpers/id_property.rb +72 -0
  44. data/lib/active_graph/migrations/helpers/relationships.rb +66 -0
  45. data/lib/active_graph/migrations/helpers/schema.rb +63 -0
  46. data/lib/active_graph/migrations/migration_file.rb +24 -0
  47. data/lib/active_graph/migrations/runner.rb +195 -0
  48. data/lib/active_graph/migrations/schema.rb +64 -0
  49. data/lib/active_graph/migrations/schema_migration.rb +14 -0
  50. data/lib/active_graph/model_schema.rb +139 -0
  51. data/lib/active_graph/node.rb +110 -0
  52. data/lib/active_graph/node/callbacks.rb +8 -0
  53. data/lib/active_graph/node/dependent.rb +11 -0
  54. data/lib/active_graph/node/dependent/association_methods.rb +49 -0
  55. data/lib/active_graph/node/dependent/query_proxy_methods.rb +52 -0
  56. data/lib/active_graph/node/dependent_callbacks.rb +31 -0
  57. data/lib/active_graph/node/enum.rb +26 -0
  58. data/lib/active_graph/node/has_n.rb +602 -0
  59. data/lib/active_graph/node/has_n/association.rb +278 -0
  60. data/lib/active_graph/node/has_n/association/rel_factory.rb +61 -0
  61. data/lib/active_graph/node/has_n/association/rel_wrapper.rb +23 -0
  62. data/lib/active_graph/node/has_n/association_cypher_methods.rb +108 -0
  63. data/lib/active_graph/node/id_property.rb +224 -0
  64. data/lib/active_graph/node/id_property/accessor.rb +62 -0
  65. data/lib/active_graph/node/initialize.rb +21 -0
  66. data/lib/active_graph/node/labels.rb +207 -0
  67. data/lib/active_graph/node/labels/index.rb +37 -0
  68. data/lib/active_graph/node/labels/reloading.rb +21 -0
  69. data/lib/active_graph/node/node_list_formatter.rb +13 -0
  70. data/lib/active_graph/node/node_wrapper.rb +54 -0
  71. data/lib/active_graph/node/orm_adapter.rb +82 -0
  72. data/lib/active_graph/node/persistence.rb +186 -0
  73. data/lib/active_graph/node/property.rb +60 -0
  74. data/lib/active_graph/node/query.rb +76 -0
  75. data/lib/active_graph/node/query/query_proxy.rb +367 -0
  76. data/lib/active_graph/node/query/query_proxy_eager_loading.rb +177 -0
  77. data/lib/active_graph/node/query/query_proxy_eager_loading/association_tree.rb +75 -0
  78. data/lib/active_graph/node/query/query_proxy_enumerable.rb +110 -0
  79. data/lib/active_graph/node/query/query_proxy_find_in_batches.rb +19 -0
  80. data/lib/active_graph/node/query/query_proxy_link.rb +139 -0
  81. data/lib/active_graph/node/query/query_proxy_methods.rb +303 -0
  82. data/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb +99 -0
  83. data/lib/active_graph/node/query_methods.rb +68 -0
  84. data/lib/active_graph/node/reflection.rb +86 -0
  85. data/lib/active_graph/node/rels.rb +11 -0
  86. data/lib/active_graph/node/scope.rb +166 -0
  87. data/lib/active_graph/node/unpersisted.rb +48 -0
  88. data/lib/active_graph/node/validations.rb +59 -0
  89. data/lib/active_graph/paginated.rb +27 -0
  90. data/lib/active_graph/railtie.rb +108 -0
  91. data/lib/active_graph/relationship.rb +68 -0
  92. data/lib/active_graph/relationship/callbacks.rb +21 -0
  93. data/lib/active_graph/relationship/initialize.rb +28 -0
  94. data/lib/active_graph/relationship/persistence.rb +133 -0
  95. data/lib/active_graph/relationship/persistence/query_factory.rb +95 -0
  96. data/lib/active_graph/relationship/property.rb +92 -0
  97. data/lib/active_graph/relationship/query.rb +99 -0
  98. data/lib/active_graph/relationship/rel_wrapper.rb +31 -0
  99. data/lib/active_graph/relationship/related_node.rb +87 -0
  100. data/lib/active_graph/relationship/types.rb +80 -0
  101. data/lib/active_graph/relationship/validations.rb +8 -0
  102. data/lib/active_graph/schema/operation.rb +102 -0
  103. data/lib/active_graph/shared.rb +48 -0
  104. data/lib/active_graph/shared/attributes.rb +217 -0
  105. data/lib/active_graph/shared/callbacks.rb +66 -0
  106. data/lib/active_graph/shared/cypher.rb +37 -0
  107. data/lib/active_graph/shared/declared_properties.rb +204 -0
  108. data/lib/active_graph/shared/declared_property.rb +109 -0
  109. data/lib/active_graph/shared/declared_property/index.rb +37 -0
  110. data/lib/active_graph/shared/enum.rb +167 -0
  111. data/lib/active_graph/shared/filtered_hash.rb +79 -0
  112. data/lib/active_graph/shared/identity.rb +34 -0
  113. data/lib/active_graph/shared/initialize.rb +65 -0
  114. data/lib/active_graph/shared/marshal.rb +23 -0
  115. data/lib/active_graph/shared/mass_assignment.rb +63 -0
  116. data/lib/active_graph/shared/permitted_attributes.rb +28 -0
  117. data/lib/active_graph/shared/persistence.rb +272 -0
  118. data/lib/active_graph/shared/property.rb +249 -0
  119. data/lib/active_graph/shared/query_factory.rb +122 -0
  120. data/lib/active_graph/shared/rel_type_converters.rb +43 -0
  121. data/lib/active_graph/shared/serialized_properties.rb +30 -0
  122. data/lib/active_graph/shared/type_converters.rb +439 -0
  123. data/lib/active_graph/shared/typecasted_attributes.rb +99 -0
  124. data/lib/active_graph/shared/typecaster.rb +53 -0
  125. data/lib/active_graph/shared/validations.rb +44 -0
  126. data/lib/active_graph/tasks/migration.rake +204 -0
  127. data/lib/active_graph/timestamps.rb +11 -0
  128. data/lib/active_graph/timestamps/created.rb +9 -0
  129. data/lib/active_graph/timestamps/updated.rb +9 -0
  130. data/lib/active_graph/transaction.rb +22 -0
  131. data/lib/active_graph/transactions.rb +57 -0
  132. data/lib/active_graph/type_converters.rb +7 -0
  133. data/lib/active_graph/undeclared_properties.rb +53 -0
  134. data/lib/active_graph/version.rb +3 -0
  135. data/lib/active_graph/wrapper.rb +4 -0
  136. data/lib/rails/generators/active_graph/migration/migration_generator.rb +16 -0
  137. data/lib/rails/generators/active_graph/migration/templates/migration.erb +9 -0
  138. data/lib/rails/generators/active_graph/model/model_generator.rb +89 -0
  139. data/lib/rails/generators/active_graph/model/templates/migration.erb +11 -0
  140. data/lib/rails/generators/active_graph/model/templates/model.erb +15 -0
  141. data/lib/rails/generators/active_graph/upgrade_v8/templates/migration.erb +17 -0
  142. data/lib/rails/generators/active_graph/upgrade_v8/upgrade_v8_generator.rb +34 -0
  143. data/lib/rails/generators/active_graph_generator.rb +121 -0
  144. metadata +423 -0
@@ -0,0 +1,8 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Callbacks #:nodoc:
4
+ extend ActiveSupport::Concern
5
+ include ActiveGraph::Shared::Callbacks
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Dependent
4
+ def dependent_children
5
+ @dependent_children ||= []
6
+ end
7
+
8
+ attr_writer :called_by
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Dependent
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
+
9
+ def add_destroy_callbacks(model)
10
+ return if dependent.nil?
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
+ unique_query = object.as(:self).unique_nodes(self, :self, :n, :other_rel)
32
+ unique_query.query.optional_match('(n)-[r]-()').delete(:n, :r).exec if unique_query
33
+ end
34
+
35
+ def dependent_destroy_callback(object)
36
+ unique_query = object.association_query_proxy(name)
37
+ unique_query.each_for_destruction(object, &:destroy) if unique_query
38
+ end
39
+
40
+ def dependent_destroy_orphans_callback(object)
41
+ unique_query = object.as(:self).unique_nodes(self, :self, :n, :other_rel)
42
+ unique_query.each_for_destruction(object, &:destroy) if unique_query
43
+ end
44
+
45
+ # End callback methods
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Dependent
4
+ # methods used to resolve association dependencies
5
+ module QueryProxyMethods
6
+ # Used as part of `dependent: :destroy` and may not have any utility otherwise.
7
+ # It keeps track of the node responsible for a cascading `destroy` process.
8
+ # @param owning_node [#dependent_children] source_object The node that called this method. Typically, we would use QueryProxy's `source_object` method
9
+ # but this is not always available, so we require it explicitly.
10
+ def each_for_destruction(owning_node)
11
+ target = owning_node.called_by || owning_node
12
+ objects = pluck(identity).compact.reject do |obj|
13
+ target.dependent_children.include?(obj)
14
+ end
15
+
16
+ objects.each do |obj|
17
+ obj.called_by = target
18
+ target.dependent_children << obj
19
+ yield obj
20
+ end
21
+ end
22
+
23
+ # This will match nodes who only have a single relationship of a given type.
24
+ # It's used by `dependent: :delete_orphans` and `dependent: :destroy_orphans` and may not have much utility otherwise.
25
+ # @param [ActiveGraph::Node::HasN::Association] association The Association object used throughout the match.
26
+ # @param [String, Symbol] other_node The identifier to use for the other end of the chain.
27
+ # @param [String, Symbol] other_rel The identifier to use for the relationship in the optional match.
28
+ # @return [ActiveGraph::Node::Query::QueryProxy]
29
+ def unique_nodes(association, self_identifer, other_node, other_rel, ids = [])
30
+ fail 'Only supported by in QueryProxy chains started by an instance' unless source_object
31
+ return false if send(association.name).empty?
32
+ unique_nodes_query(association, self_identifer, other_node, other_rel, ids)
33
+ .proxy_as(association.target_class, other_node)
34
+ end
35
+
36
+ private
37
+
38
+ def unique_nodes_query(association, self_identifer, other_node, other_rel, ids)
39
+ base = query.with(identity).proxy_as_optional(source_object.class, self_identifer)
40
+ .send(association.name, other_node, other_rel)
41
+ base = base.where(id: ids) if ids.present?
42
+ base.query
43
+ .with(other_node)
44
+ .match("()#{association.arrow_cypher(:orphan_rel)}(#{other_node})")
45
+ .with(other_node, count: 'count(*)')
46
+ .where('count = $one', one: 1)
47
+ .break
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,31 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module DependentCallbacks
4
+ extend ActiveSupport::Concern
5
+
6
+ def dependent_delete_callback(association, ids)
7
+ association_query_proxy(association.name).where(id: ids).delete_all
8
+ end
9
+
10
+ def dependent_delete_orphans_callback(association, ids)
11
+ unique_query = as(:self).unique_nodes(association, :self, :n, :other_rel, ids)
12
+ unique_query.query.optional_match('(n)-[r]-()').delete(:n, :r).exec if unique_query
13
+ end
14
+
15
+ def dependent_destroy_callback(association, ids)
16
+ unique_query = association_query_proxy(association.name).where(id: ids)
17
+ unique_query.each_for_destruction(self, &:destroy) if unique_query
18
+ end
19
+
20
+ def dependent_destroy_orphans_callback(association, ids)
21
+ unique_query = as(:self).unique_nodes(association, :self, :n, :other_rel, ids)
22
+ unique_query.each_for_destruction(self, &:destroy) if unique_query
23
+ end
24
+
25
+ def callbacks_from_relationship(relationship, direction, other_node)
26
+ rel = relationship_corresponding_rel(relationship, direction, other_node.class).try(:last)
27
+ public_send("dependent_#{rel.dependent}_callback", rel, [other_node.id]) if rel && rel.dependent
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ module ActiveGraph::Node
2
+ module Enum
3
+ extend ActiveSupport::Concern
4
+ include ActiveGraph::Shared::Enum
5
+
6
+ module ClassMethods
7
+ protected
8
+
9
+ def define_property(property_name, *args)
10
+ super
11
+ ActiveGraph::ModelSchema.add_required_index(self, property_name) unless args[1][:_index] == false
12
+ end
13
+
14
+ def define_enum_methods(property_name, enum_keys, options)
15
+ super
16
+ define_enum_scopes(property_name, enum_keys)
17
+ end
18
+
19
+ def define_enum_scopes(property_name, enum_keys)
20
+ enum_keys.each_key do |name|
21
+ scope name, -> { where(property_name => name) }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,602 @@
1
+ module ActiveGraph::Node
2
+ module HasN
3
+ extend ActiveSupport::Concern
4
+
5
+ class NonPersistedNodeError < ActiveGraph::Error; end
6
+ class HasOneConstraintError < ActiveGraph::Error; 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, deferred_objects = [], result_cache_proc = nil)
12
+ @query_proxy = query_proxy
13
+ @deferred_objects = deferred_objects
14
+
15
+ @result_cache_proc = result_cache_proc
16
+
17
+ # Represents the thing which can be enumerated
18
+ # default to @query_proxy, but will be set to
19
+ # @cached_result if that is set
20
+ @enumerable = @query_proxy
21
+ end
22
+
23
+ # States:
24
+ # Default
25
+ def inspect
26
+ formatted_nodes = ::ActiveGraph::Node::NodeListFormatter.new(result_nodes)
27
+ "#<AssociationProxy #{@query_proxy.context} #{formatted_nodes.inspect}>"
28
+ end
29
+
30
+ extend Forwardable
31
+ %w(include? find first last ==).each do |delegated_method|
32
+ def_delegator :@enumerable, delegated_method
33
+ end
34
+
35
+ include Enumerable
36
+
37
+ def each(&block)
38
+ result_nodes.each(&block)
39
+ end
40
+
41
+ def each_rel(&block)
42
+ rels.each(&block)
43
+ end
44
+
45
+ # .count always hits the database
46
+ def_delegator :@query_proxy, :count
47
+
48
+ def length
49
+ @deferred_objects.length + @enumerable.length
50
+ end
51
+
52
+ def size
53
+ @deferred_objects.size + @enumerable.size
54
+ end
55
+
56
+ def empty?(*args)
57
+ @deferred_objects.empty? && @enumerable.empty?(*args)
58
+ end
59
+
60
+ def ==(other)
61
+ self.to_a == other.to_a
62
+ end
63
+
64
+ def +(other)
65
+ self.to_a + other
66
+ end
67
+
68
+ def result
69
+ (@deferred_objects || []) + result_without_deferred
70
+ end
71
+
72
+ def result_without_deferred
73
+ cache_query_proxy_result if !@cached_result
74
+
75
+ @cached_result
76
+ end
77
+
78
+ def result_nodes
79
+ return result_objects if !@query_proxy.model
80
+
81
+ map_results_as_nodes(result_objects)
82
+ end
83
+
84
+ def result_objects
85
+ @deferred_objects + result_without_deferred
86
+ end
87
+
88
+ def result_ids
89
+ result.map do |object|
90
+ object.is_a?(ActiveGraph::Node) ? object.id : object
91
+ end
92
+ end
93
+
94
+ def cache_result(result)
95
+ @cached_result = result
96
+ @enumerable = (@cached_result || @query_proxy)
97
+ end
98
+
99
+ def init_cache
100
+ @cached_rels ||= []
101
+ @cached_result ||= []
102
+ end
103
+
104
+ def add_to_cache(object, rel = nil)
105
+ (@cached_rels ||= []) << rel if rel
106
+ (@cached_result ||= []).tap { |results| results << object unless results.include?(object) }
107
+ end
108
+
109
+ def rels
110
+ @cached_rels || super
111
+ end
112
+
113
+ def cache_query_proxy_result
114
+ (result_cache_proc_cache || @query_proxy).to_a.tap { |result| cache_result(result) }
115
+ end
116
+
117
+ def result_cache_proc_cache
118
+ @result_cache_proc_cache ||= @result_cache_proc.call if @result_cache_proc
119
+ end
120
+
121
+ def clear_cache_result
122
+ cache_result(nil)
123
+ end
124
+
125
+ def cached?
126
+ !!@cached_result
127
+ end
128
+
129
+ def replace_with(*args)
130
+ nodes = @query_proxy.replace_with(*args).to_a
131
+ if @query_proxy.start_object.try(:new_record?)
132
+ @cached_result = nil
133
+ else
134
+ cache_result(nodes)
135
+ end
136
+ end
137
+
138
+ alias to_ary to_a
139
+
140
+ QUERY_PROXY_METHODS = [:<<, :delete, :create, :pluck, :where, :where_not, :rel_where, :rel_order, :order, :skip, :limit]
141
+
142
+ QUERY_PROXY_METHODS.each do |method|
143
+ define_method(method) do |*args, &block|
144
+ @query_proxy.public_send(method, *args, &block)
145
+ end
146
+ end
147
+
148
+ CACHED_RESULT_METHODS = []
149
+
150
+ def method_missing(method_name, *args, &block)
151
+ target = target_for_missing_method(method_name)
152
+ super if target.nil?
153
+
154
+ cache_query_proxy_result if !cached? && !target.is_a?(ActiveGraph::Node::Query::QueryProxy)
155
+ clear_cache_result if target.is_a?(ActiveGraph::Node::Query::QueryProxy)
156
+
157
+ target.public_send(method_name, *args, &block)
158
+ end
159
+
160
+ def serializable_hash(options = {})
161
+ to_a.map { |record| record.serializable_hash(options) }
162
+ end
163
+
164
+ private
165
+
166
+ def map_results_as_nodes(result)
167
+ result.map do |object|
168
+ object.is_a?(ActiveGraph::Node) ? object : @query_proxy.model.find(object)
169
+ end
170
+ end
171
+
172
+ def target_for_missing_method(method_name)
173
+ case method_name
174
+ when *CACHED_RESULT_METHODS
175
+ @cached_result
176
+ else
177
+ if @cached_result && @cached_result.respond_to?(method_name)
178
+ @cached_result
179
+ elsif @query_proxy.respond_to?(method_name)
180
+ @query_proxy
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ # Returns the current AssociationProxy cache for the association cache. It is in the format
187
+ # { :association_name => AssociationProxy}
188
+ # This is so that we
189
+ # * don't need to re-build the QueryProxy objects
190
+ # * also because the QueryProxy object caches it's results
191
+ # * so we don't need to query again
192
+ # * so that we can cache results from association calls or eager loading
193
+ def association_proxy_cache
194
+ @association_proxy_cache ||= {}
195
+ end
196
+
197
+ def association_proxy_cache_fetch(key)
198
+ association_proxy_cache.fetch(key) do
199
+ value = yield
200
+ association_proxy_cache[key] = value
201
+ end
202
+ end
203
+
204
+ def association_query_proxy(name, options = {})
205
+ self.class.send(:association_query_proxy, name, {start_object: self}.merge!(options))
206
+ end
207
+
208
+ def association_proxy_hash(name, options = {})
209
+ [name.to_sym, options.values_at(:node, :rel, :labels, :rel_length)].hash
210
+ end
211
+
212
+ def association_proxy(name, options = {})
213
+ name = name.to_sym
214
+ hash = association_proxy_hash(name, options)
215
+ association_proxy_cache_fetch(hash) do
216
+ if result_cache = self.instance_variable_get('@source_proxy_result_cache')
217
+ cache = nil
218
+ result_cache.inject(nil) do |proxy_to_return, object|
219
+ proxy = fresh_association_proxy(name, options.merge(start_object: object),
220
+ proc { (cache ||= previous_proxy_results_by_previous_id(result_cache, name))[object.neo_id] })
221
+
222
+ object.association_proxy_cache[hash] = proxy
223
+
224
+ (self == object ? proxy : proxy_to_return)
225
+ end
226
+ else
227
+ fresh_association_proxy(name, options)
228
+ end
229
+ end
230
+ end
231
+
232
+ def delete_reverse_has_one_core_rel(association)
233
+ reverse_assoc = reverse_association(association)
234
+ delete_has_one_rel!(reverse_assoc) if reverse_assoc && reverse_assoc.type == :has_one
235
+ end
236
+
237
+ def reverse_association(association)
238
+ reverse_assoc = self.class.associations.find do |_key, assoc|
239
+ association.inverse_of?(assoc) || assoc.inverse_of?(association)
240
+ end
241
+ reverse_assoc && reverse_assoc.last
242
+ end
243
+
244
+ def delete_reverse_has_one_relationship(relationship, direction, other_node)
245
+ rel = relationship_corresponding_rel(relationship, direction, other_node.class)
246
+ delete_has_one_rel!(rel.last) if rel && rel.last.type == :has_one
247
+ end
248
+
249
+ def delete_has_one_rel!(rel)
250
+ send("#{rel.name}", :n, :r, chainable: true).query.delete(:r).exec
251
+ association_proxy_cache.clear
252
+ end
253
+
254
+ def relationship_corresponding_rel(relationship, direction, target_class)
255
+ self.class.associations.find do |_key, assoc|
256
+ assoc.relationship_class_name == relationship.class.name ||
257
+ (assoc.relationship_type == relationship.type.to_sym && assoc.target_class == target_class && assoc.direction == direction)
258
+ end
259
+ end
260
+
261
+ private
262
+
263
+ def fresh_association_proxy(name, options = {}, result_cache_proc = nil)
264
+ AssociationProxy.new(association_query_proxy(name, options), deferred_nodes_for_association(name), result_cache_proc)
265
+ end
266
+
267
+ def previous_proxy_results_by_previous_id(result_cache, association_name)
268
+ query_proxy = self.class.as(:previous).where(neo_id: result_cache.map(&:neo_id))
269
+ query_proxy = self.class.send(:association_query_proxy, association_name, previous_query_proxy: query_proxy, node: :next, optional: true)
270
+
271
+ Hash[*query_proxy.pluck('ID(previous)', 'collect(next)').flatten(1)].each_value do |records|
272
+ records.each do |record|
273
+ record.instance_variable_set('@source_proxy_result_cache', records)
274
+ end
275
+ end
276
+ end
277
+
278
+ # rubocop:disable Metrics/ModuleLength
279
+ module ClassMethods
280
+ # rubocop:disable Naming/PredicateName
281
+
282
+ # :nocov:
283
+ def has_association?(name)
284
+ ActiveSupport::Deprecation.warn 'has_association? is deprecated and may be removed from future releases, use association? instead.', caller
285
+
286
+ association?(name)
287
+ end
288
+ # :nocov:
289
+
290
+ # rubocop:enable Naming/PredicateName
291
+
292
+ def association?(name)
293
+ !!associations[name.to_sym]
294
+ end
295
+
296
+ def parent_associations
297
+ superclass == Object ? {} : superclass.associations
298
+ end
299
+
300
+ def associations
301
+ (@associations ||= parent_associations.dup)
302
+ end
303
+
304
+ def associations_keys
305
+ @associations_keys ||= associations.keys
306
+ end
307
+
308
+ # For defining an "has many" association on a model. This defines a set of methods on
309
+ # your model instances. For instance, if you define the association on a Person model:
310
+ #
311
+ #
312
+ # .. code-block:: ruby
313
+ #
314
+ # has_many :out, :vehicles, type: :has_vehicle
315
+ #
316
+ # This would define the following methods:
317
+ #
318
+ # **#vehicles**
319
+ # Returns a QueryProxy object. This is an Enumerable object and thus can be iterated
320
+ # over. It also has the ability to accept class-level methods from the Vehicle model
321
+ # (including calls to association methods)
322
+ #
323
+ # **#vehicles=**
324
+ # Takes an array of Vehicle objects and replaces all current ``:HAS_VEHICLE`` relationships
325
+ # with new relationships refering to the specified objects
326
+ #
327
+ # **.vehicles**
328
+ # Returns a QueryProxy object. This would represent all ``Vehicle`` objects associated with
329
+ # either all ``Person`` nodes (if ``Person.vehicles`` is called), or all ``Vehicle`` objects
330
+ # associated with the ``Person`` nodes thus far represented in the QueryProxy chain.
331
+ # For example:
332
+ #
333
+ # .. code-block:: ruby
334
+ #
335
+ # company.people.where(age: 40).vehicles
336
+ #
337
+ # Arguments:
338
+ # **direction:**
339
+ # **Available values:** ``:in``, ``:out``, or ``:both``.
340
+ #
341
+ # Refers to the relative to the model on which the association is being defined.
342
+ #
343
+ # Example:
344
+ #
345
+ # .. code-block:: ruby
346
+ #
347
+ # Person.has_many :out, :posts, type: :wrote
348
+ #
349
+ # means that a `WROTE` relationship goes from a `Person` node to a `Post` node
350
+ #
351
+ # **name:**
352
+ # The name of the association. The affects the methods which are created (see above).
353
+ # The name is also used to form default assumptions about the model which is being referred to
354
+ #
355
+ # Example:
356
+ #
357
+ # .. code-block:: ruby
358
+ #
359
+ # Person.has_many :out, :posts, type: :wrote
360
+ #
361
+ # will assume a `model_class` option of ``'Post'`` unless otherwise specified
362
+ #
363
+ # **options:** A ``Hash`` of options. Allowed keys are:
364
+ # *type*: The Neo4j relationship type. This option is required unless either the
365
+ # `origin` or `rel_class` options are specified
366
+ #
367
+ # *origin*: The name of the association from another model which the `type` and `model_class`
368
+ # can be gathered.
369
+ #
370
+ # Example:
371
+ #
372
+ # .. code-block:: ruby
373
+ #
374
+ # # `model_class` of `Post` is assumed here
375
+ # Person.has_many :out, :posts, origin: :author
376
+ #
377
+ # Post.has_one :in, :author, type: :has_author, model_class: :Person
378
+ #
379
+ # *model_class*: The model class to which the association is referring. Can be a
380
+ # Symbol/String (or an ``Array`` of same) with the name of the `Node` class,
381
+ # `false` to specify any model, or nil to specify that it should be guessed.
382
+ #
383
+ # *rel_class*: The ``Relationship`` class to use for this association. Can be either a
384
+ # model object ``include`` ing ``Relationship`` or a Symbol/String (or an ``Array`` of same).
385
+ # **A Symbol or String is recommended** to avoid load-time issues
386
+ #
387
+ # *dependent*: Enables deletion cascading.
388
+ # **Available values:** ``:delete``, ``:delete_orphans``, ``:destroy``, ``:destroy_orphans``
389
+ # (note that the ``:destroy_orphans`` option is known to be "very metal". Caution advised)
390
+ #
391
+ def has_many(direction, name, options = {}) # rubocop:disable Naming/PredicateName
392
+ name = name.to_sym
393
+ build_association(:has_many, direction, name, options)
394
+
395
+ define_has_many_methods(name, options)
396
+ end
397
+
398
+ # For defining an "has one" association on a model. This defines a set of methods on
399
+ # your model instances. For instance, if you define the association on a Person model:
400
+ #
401
+ # has_one :out, :vehicle, type: :has_vehicle
402
+ #
403
+ # This would define the methods: ``#vehicle``, ``#vehicle=``, and ``.vehicle``.
404
+ #
405
+ # See :ref:`#has_many <ActiveGraph/Node/HasN/ClassMethods#has_many>` for anything
406
+ # not specified here
407
+ #
408
+ def has_one(direction, name, options = {}) # rubocop:disable Naming/PredicateName
409
+ name = name.to_sym
410
+ build_association(:has_one, direction, name, options)
411
+
412
+ define_has_one_methods(name, options)
413
+ end
414
+
415
+ private
416
+
417
+ def define_has_many_methods(name, association_options)
418
+ default_options = association_options.slice(:labels)
419
+
420
+ define_method(name) do |node = nil, rel = nil, options = {}|
421
+ # return [].freeze unless self._persisted_obj
422
+
423
+ options, node = node, nil if node.is_a?(Hash)
424
+
425
+ options = default_options.merge(options)
426
+
427
+ association_proxy(name, {node: node, rel: rel, source_object: self, labels: options[:labels]}.merge!(options))
428
+ end
429
+
430
+ define_has_many_setter(name)
431
+
432
+ define_has_many_id_methods(name)
433
+
434
+ define_class_method(name) do |node = nil, rel = nil, options = {}|
435
+ options, node = node, nil if node.is_a?(Hash)
436
+
437
+ options = default_options.merge(options)
438
+
439
+ association_proxy(name, {node: node, rel: rel, labels: options[:labels]}.merge!(options))
440
+ end
441
+ end
442
+
443
+ def define_has_many_setter(name)
444
+ define_setter(name, "#{name}=")
445
+ end
446
+
447
+ def define_has_many_id_methods(name)
448
+ define_method_unless_defined("#{name.to_s.singularize}_ids") do
449
+ association_proxy(name).result_ids
450
+ end
451
+
452
+ define_setter(name, "#{name.to_s.singularize}_ids=")
453
+
454
+ define_method_unless_defined("#{name.to_s.singularize}_neo_ids") do
455
+ association_proxy(name).pluck(:neo_id)
456
+ end
457
+ end
458
+
459
+ def define_setter(name, setter_name)
460
+ define_method_unless_defined(setter_name) do |others|
461
+ association_proxy_cache.clear # TODO: Should probably just clear for this association...
462
+ clear_deferred_nodes_for_association(name)
463
+ others = Array(others).reject(&:blank?)
464
+ if persisted?
465
+ ActiveGraph::Base.transaction { association_proxy(name).replace_with(others) }
466
+ else
467
+ defer_create(name, others, clear: true)
468
+ end
469
+ end
470
+ end
471
+
472
+ def define_method_unless_defined(method_name, &block)
473
+ define_method(method_name, block) unless method_defined?(method_name)
474
+ end
475
+
476
+ def define_has_one_methods(name, association_options)
477
+ default_options = association_options.slice(:labels)
478
+
479
+ define_has_one_getter(name, default_options)
480
+
481
+ define_has_one_setter(name)
482
+
483
+ define_has_one_id_methods(name)
484
+
485
+ define_class_method(name) do |node = nil, rel = nil, options = {}|
486
+ options, node = node, nil if node.is_a?(Hash)
487
+
488
+ options = default_options.merge(options)
489
+
490
+ association_proxy(name, {node: node, rel: rel, labels: options[:labels]}.merge!(options))
491
+ end
492
+ end
493
+
494
+ def define_has_one_id_methods(name)
495
+ define_method_unless_defined("#{name}_id") do
496
+ association_proxy(name).result_ids.first
497
+ end
498
+
499
+ define_setter(name, "#{name}_id=")
500
+
501
+ define_method_unless_defined("#{name}_neo_id") do
502
+ association_proxy(name).pluck(:neo_id).first
503
+ end
504
+ end
505
+
506
+ def define_has_one_getter(name, default_options)
507
+ define_method(name) do |node = nil, rel = nil, options = {}|
508
+ options, node = node, nil if node.is_a?(Hash)
509
+
510
+ options = default_options.merge(options)
511
+
512
+ association_proxy = association_proxy(name, {node: node, rel: rel}.merge!(options))
513
+
514
+ # Return all results if options[:chainable] == true or a variable-length relationship length was given
515
+ if options[:chainable] || (options[:rel_length] && !options[:rel_length].is_a?(Integer))
516
+ association_proxy
517
+ else
518
+ o = association_proxy.result.first
519
+ self.class.send(:association_target_class, name).try(:nodeify, o) || o
520
+ end
521
+ end
522
+ end
523
+
524
+ def define_has_one_setter(name)
525
+ define_setter(name, "#{name}=")
526
+ end
527
+
528
+ def define_class_method(*args, &block)
529
+ klass = class << self; self; end
530
+ klass.instance_eval do
531
+ define_method(*args, &block)
532
+ end
533
+ end
534
+
535
+ def association_query_proxy(name, options = {})
536
+ previous_query_proxy = options[:previous_query_proxy] || current_scope
537
+ query_proxy = previous_query_proxy || default_association_query_proxy
538
+ ActiveGraph::Node::Query::QueryProxy.new(association_target_class(name),
539
+ associations[name],
540
+ {query_proxy: query_proxy,
541
+ context: "#{query_proxy.context || self.name}##{name}",
542
+ optional: query_proxy.optional?,
543
+ association_labels: options[:labels],
544
+ source_object: query_proxy.source_object}.merge!(options)).tap do |query_proxy_result|
545
+ target_classes = association_target_classes(name)
546
+ return query_proxy_result.as_models(target_classes) if target_classes
547
+ end
548
+ end
549
+
550
+ def association_proxy(name, options = {})
551
+ AssociationProxy.new(association_query_proxy(name, options))
552
+ end
553
+
554
+ def association_target_class(name)
555
+ target_classes_or_nil = associations[name].target_classes_or_nil
556
+
557
+ return if !target_classes_or_nil.is_a?(Array) || target_classes_or_nil.size != 1
558
+
559
+ target_classes_or_nil[0]
560
+ end
561
+
562
+ def association_target_classes(name)
563
+ target_classes_or_nil = associations[name].target_classes_or_nil
564
+
565
+ return if !target_classes_or_nil.is_a?(Array) || target_classes_or_nil.size <= 1
566
+
567
+ target_classes_or_nil
568
+ end
569
+
570
+ def default_association_query_proxy
571
+ ActiveGraph::Node::Query::QueryProxy.new("::#{self.name}".constantize, nil,
572
+ query_proxy: nil, context: self.name.to_s)
573
+ end
574
+
575
+ def build_association(macro, direction, name, options)
576
+ options[:model_class] = options[:model_class].name if options[:model_class] == self
577
+ ActiveGraph::Node::HasN::Association.new(macro, direction, name, options).tap do |association|
578
+ add_association(name, association)
579
+ create_reflection(macro, name, association, self)
580
+ end
581
+
582
+ @associations_keys = nil
583
+
584
+ # Re-raise any exception with added class name and association name to
585
+ # make sure error message is helpful
586
+ rescue StandardError => e
587
+ raise e.class, "#{e.message} (#{self.class}##{name})"
588
+ end
589
+
590
+ def add_association(name, association_object)
591
+ fail "Association `#{name}` defined for a second time. "\
592
+ 'Associations can only be defined once' if duplicate_association?(name)
593
+ associations[name] = association_object
594
+ end
595
+
596
+ def duplicate_association?(name)
597
+ associations.key?(name) && parent_associations[name] != associations[name]
598
+ end
599
+ end
600
+ # rubocop:enable Metrics/ModuleLength
601
+ end
602
+ end