activegraph 11.0.0.beta.1-java

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