activegraph 10.0.0.pre.alpha.6

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 (142) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1989 -0
  3. data/CONTRIBUTORS +12 -0
  4. data/Gemfile +24 -0
  5. data/README.md +107 -0
  6. data/bin/rake +17 -0
  7. data/config/locales/en.yml +5 -0
  8. data/config/neo4j/add_classnames.yml +1 -0
  9. data/config/neo4j/config.yml +38 -0
  10. data/lib/neo4j.rb +116 -0
  11. data/lib/neo4j/active_base.rb +89 -0
  12. data/lib/neo4j/active_node.rb +108 -0
  13. data/lib/neo4j/active_node/callbacks.rb +8 -0
  14. data/lib/neo4j/active_node/dependent.rb +11 -0
  15. data/lib/neo4j/active_node/dependent/association_methods.rb +49 -0
  16. data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +51 -0
  17. data/lib/neo4j/active_node/enum.rb +26 -0
  18. data/lib/neo4j/active_node/has_n.rb +612 -0
  19. data/lib/neo4j/active_node/has_n/association.rb +278 -0
  20. data/lib/neo4j/active_node/has_n/association/rel_factory.rb +61 -0
  21. data/lib/neo4j/active_node/has_n/association/rel_wrapper.rb +23 -0
  22. data/lib/neo4j/active_node/has_n/association_cypher_methods.rb +108 -0
  23. data/lib/neo4j/active_node/id_property.rb +224 -0
  24. data/lib/neo4j/active_node/id_property/accessor.rb +62 -0
  25. data/lib/neo4j/active_node/initialize.rb +21 -0
  26. data/lib/neo4j/active_node/labels.rb +207 -0
  27. data/lib/neo4j/active_node/labels/index.rb +37 -0
  28. data/lib/neo4j/active_node/labels/reloading.rb +21 -0
  29. data/lib/neo4j/active_node/node_list_formatter.rb +13 -0
  30. data/lib/neo4j/active_node/node_wrapper.rb +54 -0
  31. data/lib/neo4j/active_node/orm_adapter.rb +82 -0
  32. data/lib/neo4j/active_node/persistence.rb +187 -0
  33. data/lib/neo4j/active_node/property.rb +60 -0
  34. data/lib/neo4j/active_node/query.rb +76 -0
  35. data/lib/neo4j/active_node/query/query_proxy.rb +374 -0
  36. data/lib/neo4j/active_node/query/query_proxy_eager_loading.rb +177 -0
  37. data/lib/neo4j/active_node/query/query_proxy_eager_loading/association_tree.rb +75 -0
  38. data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +110 -0
  39. data/lib/neo4j/active_node/query/query_proxy_find_in_batches.rb +19 -0
  40. data/lib/neo4j/active_node/query/query_proxy_link.rb +139 -0
  41. data/lib/neo4j/active_node/query/query_proxy_methods.rb +302 -0
  42. data/lib/neo4j/active_node/query/query_proxy_methods_of_mass_updating.rb +86 -0
  43. data/lib/neo4j/active_node/query_methods.rb +68 -0
  44. data/lib/neo4j/active_node/reflection.rb +86 -0
  45. data/lib/neo4j/active_node/rels.rb +11 -0
  46. data/lib/neo4j/active_node/scope.rb +166 -0
  47. data/lib/neo4j/active_node/unpersisted.rb +48 -0
  48. data/lib/neo4j/active_node/validations.rb +59 -0
  49. data/lib/neo4j/active_rel.rb +67 -0
  50. data/lib/neo4j/active_rel/callbacks.rb +15 -0
  51. data/lib/neo4j/active_rel/initialize.rb +28 -0
  52. data/lib/neo4j/active_rel/persistence.rb +134 -0
  53. data/lib/neo4j/active_rel/persistence/query_factory.rb +95 -0
  54. data/lib/neo4j/active_rel/property.rb +95 -0
  55. data/lib/neo4j/active_rel/query.rb +101 -0
  56. data/lib/neo4j/active_rel/rel_wrapper.rb +31 -0
  57. data/lib/neo4j/active_rel/related_node.rb +87 -0
  58. data/lib/neo4j/active_rel/types.rb +82 -0
  59. data/lib/neo4j/active_rel/validations.rb +8 -0
  60. data/lib/neo4j/ansi.rb +14 -0
  61. data/lib/neo4j/class_arguments.rb +39 -0
  62. data/lib/neo4j/config.rb +135 -0
  63. data/lib/neo4j/core.rb +14 -0
  64. data/lib/neo4j/core/connection_failed_error.rb +6 -0
  65. data/lib/neo4j/core/cypher_error.rb +37 -0
  66. data/lib/neo4j/core/driver.rb +66 -0
  67. data/lib/neo4j/core/has_uri.rb +63 -0
  68. data/lib/neo4j/core/instrumentable.rb +36 -0
  69. data/lib/neo4j/core/label.rb +158 -0
  70. data/lib/neo4j/core/logging.rb +44 -0
  71. data/lib/neo4j/core/node.rb +23 -0
  72. data/lib/neo4j/core/querable.rb +88 -0
  73. data/lib/neo4j/core/query.rb +487 -0
  74. data/lib/neo4j/core/query_builder.rb +32 -0
  75. data/lib/neo4j/core/query_clauses.rb +727 -0
  76. data/lib/neo4j/core/query_ext.rb +24 -0
  77. data/lib/neo4j/core/query_find_in_batches.rb +49 -0
  78. data/lib/neo4j/core/relationship.rb +13 -0
  79. data/lib/neo4j/core/responses.rb +50 -0
  80. data/lib/neo4j/core/result.rb +33 -0
  81. data/lib/neo4j/core/schema.rb +30 -0
  82. data/lib/neo4j/core/schema_errors.rb +12 -0
  83. data/lib/neo4j/core/wrappable.rb +30 -0
  84. data/lib/neo4j/errors.rb +57 -0
  85. data/lib/neo4j/migration.rb +148 -0
  86. data/lib/neo4j/migrations.rb +27 -0
  87. data/lib/neo4j/migrations/base.rb +77 -0
  88. data/lib/neo4j/migrations/check_pending.rb +20 -0
  89. data/lib/neo4j/migrations/helpers.rb +105 -0
  90. data/lib/neo4j/migrations/helpers/id_property.rb +75 -0
  91. data/lib/neo4j/migrations/helpers/relationships.rb +66 -0
  92. data/lib/neo4j/migrations/helpers/schema.rb +51 -0
  93. data/lib/neo4j/migrations/migration_file.rb +24 -0
  94. data/lib/neo4j/migrations/runner.rb +195 -0
  95. data/lib/neo4j/migrations/schema.rb +44 -0
  96. data/lib/neo4j/migrations/schema_migration.rb +14 -0
  97. data/lib/neo4j/model_schema.rb +139 -0
  98. data/lib/neo4j/paginated.rb +27 -0
  99. data/lib/neo4j/railtie.rb +105 -0
  100. data/lib/neo4j/schema/operation.rb +102 -0
  101. data/lib/neo4j/shared.rb +60 -0
  102. data/lib/neo4j/shared/attributes.rb +216 -0
  103. data/lib/neo4j/shared/callbacks.rb +68 -0
  104. data/lib/neo4j/shared/cypher.rb +37 -0
  105. data/lib/neo4j/shared/declared_properties.rb +204 -0
  106. data/lib/neo4j/shared/declared_property.rb +109 -0
  107. data/lib/neo4j/shared/declared_property/index.rb +37 -0
  108. data/lib/neo4j/shared/enum.rb +167 -0
  109. data/lib/neo4j/shared/filtered_hash.rb +79 -0
  110. data/lib/neo4j/shared/identity.rb +34 -0
  111. data/lib/neo4j/shared/initialize.rb +64 -0
  112. data/lib/neo4j/shared/marshal.rb +23 -0
  113. data/lib/neo4j/shared/mass_assignment.rb +64 -0
  114. data/lib/neo4j/shared/permitted_attributes.rb +28 -0
  115. data/lib/neo4j/shared/persistence.rb +282 -0
  116. data/lib/neo4j/shared/property.rb +240 -0
  117. data/lib/neo4j/shared/query_factory.rb +102 -0
  118. data/lib/neo4j/shared/rel_type_converters.rb +43 -0
  119. data/lib/neo4j/shared/serialized_properties.rb +30 -0
  120. data/lib/neo4j/shared/type_converters.rb +433 -0
  121. data/lib/neo4j/shared/typecasted_attributes.rb +98 -0
  122. data/lib/neo4j/shared/typecaster.rb +53 -0
  123. data/lib/neo4j/shared/validations.rb +44 -0
  124. data/lib/neo4j/tasks/migration.rake +202 -0
  125. data/lib/neo4j/timestamps.rb +11 -0
  126. data/lib/neo4j/timestamps/created.rb +9 -0
  127. data/lib/neo4j/timestamps/updated.rb +9 -0
  128. data/lib/neo4j/transaction.rb +139 -0
  129. data/lib/neo4j/type_converters.rb +7 -0
  130. data/lib/neo4j/undeclared_properties.rb +53 -0
  131. data/lib/neo4j/version.rb +3 -0
  132. data/lib/neo4j/wrapper.rb +4 -0
  133. data/lib/rails/generators/neo4j/migration/migration_generator.rb +14 -0
  134. data/lib/rails/generators/neo4j/migration/templates/migration.erb +9 -0
  135. data/lib/rails/generators/neo4j/model/model_generator.rb +88 -0
  136. data/lib/rails/generators/neo4j/model/templates/migration.erb +9 -0
  137. data/lib/rails/generators/neo4j/model/templates/model.erb +15 -0
  138. data/lib/rails/generators/neo4j/upgrade_v8/templates/migration.erb +17 -0
  139. data/lib/rails/generators/neo4j/upgrade_v8/upgrade_v8_generator.rb +32 -0
  140. data/lib/rails/generators/neo4j_generator.rb +119 -0
  141. data/neo4j.gemspec +51 -0
  142. metadata +421 -0
@@ -0,0 +1,8 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Callbacks #:nodoc:
4
+ extend ActiveSupport::Concern
5
+ include Neo4j::Shared::Callbacks
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module Neo4j
2
+ module ActiveNode
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 Neo4j
2
+ module ActiveNode
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,51 @@
1
+ module Neo4j
2
+ module ActiveNode
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 [Neo4j::ActiveNode::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 [Neo4j::ActiveNode::Query::QueryProxy]
29
+ def unique_nodes(association, self_identifer, other_node, other_rel)
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)
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)
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(:orphan_rel)}(#{other_node})")
44
+ .with(other_node, count: 'count(*)')
45
+ .where('count = {one}', one: 1)
46
+ .break
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ module Neo4j::ActiveNode
2
+ module Enum
3
+ extend ActiveSupport::Concern
4
+ include Neo4j::Shared::Enum
5
+
6
+ module ClassMethods
7
+ protected
8
+
9
+ def define_property(property_name, *args)
10
+ super
11
+ Neo4j::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,612 @@
1
+ module Neo4j::ActiveNode
2
+ module HasN
3
+ extend ActiveSupport::Concern
4
+
5
+ class NonPersistedNodeError < Neo4j::Error; end
6
+ class HasOneConstraintError < Neo4j::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 = ::Neo4j::ActiveNode::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?(Neo4j::ActiveNode) ? 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?(Neo4j::ActiveNode::Query::QueryProxy)
155
+ clear_cache_result if target.is_a?(Neo4j::ActiveNode::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?(Neo4j::ActiveNode) ? 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 validate_reverse_has_one_core_rel(association, other_node)
233
+ return unless Neo4j::Config[:enforce_has_one]
234
+ reverse_assoc = reverse_association(association)
235
+ validate_has_one_rel!(reverse_assoc, other_node) if reverse_assoc && reverse_assoc.type == :has_one
236
+ end
237
+
238
+ def reverse_association(association)
239
+ reverse_assoc = self.class.associations.find do |_key, assoc|
240
+ association.inverse_of?(assoc) || assoc.inverse_of?(association)
241
+ end
242
+ reverse_assoc && reverse_assoc.last
243
+ end
244
+
245
+ def validate_reverse_has_one_active_rel(active_rel, direction, other_node)
246
+ rel = active_rel_corresponding_rel(active_rel, direction, other_node.class)
247
+ validate_has_one_rel!(rel.last, other_node) if rel && rel.last.type == :has_one
248
+ end
249
+
250
+ def validate_has_one_rel!(rel, other_node)
251
+ raise_error = (node = send(rel.name.to_s)) && node != other_node
252
+ fail(HasOneConstraintError, "node #{self.class}##{neo_id} has a has_one relationship with #{other_node.class}##{other_node.neo_id}") if raise_error
253
+ end
254
+
255
+ def active_rel_corresponding_rel(active_rel, direction, target_class)
256
+ self.class.associations.find do |_key, assoc|
257
+ assoc.relationship_class_name == active_rel.class.name ||
258
+ (assoc.relationship_type == active_rel.type.to_sym && assoc.target_class == target_class && assoc.direction == direction)
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ def fresh_association_proxy(name, options = {}, result_cache_proc = nil)
265
+ AssociationProxy.new(association_query_proxy(name, options), deferred_nodes_for_association(name), result_cache_proc)
266
+ end
267
+
268
+ def previous_proxy_results_by_previous_id(result_cache, association_name)
269
+ query_proxy = self.class.as(:previous).where(neo_id: result_cache.map(&:neo_id))
270
+ query_proxy = self.class.send(:association_query_proxy, association_name, previous_query_proxy: query_proxy, node: :next, optional: true)
271
+
272
+ Hash[*query_proxy.pluck('ID(previous)', 'collect(next)').flatten(1)].each_value do |records|
273
+ records.each do |record|
274
+ record.instance_variable_set('@source_proxy_result_cache', records)
275
+ end
276
+ end
277
+ end
278
+
279
+ # rubocop:disable Metrics/ModuleLength
280
+ module ClassMethods
281
+ # rubocop:disable Naming/PredicateName
282
+
283
+ # :nocov:
284
+ def has_association?(name)
285
+ ActiveSupport::Deprecation.warn 'has_association? is deprecated and may be removed from future releases, use association? instead.', caller
286
+
287
+ association?(name)
288
+ end
289
+ # :nocov:
290
+
291
+ # rubocop:enable Naming/PredicateName
292
+
293
+ def association?(name)
294
+ !!associations[name.to_sym]
295
+ end
296
+
297
+ def parent_associations
298
+ superclass == Object ? {} : superclass.associations
299
+ end
300
+
301
+ def associations
302
+ (@associations ||= parent_associations.dup)
303
+ end
304
+
305
+ def associations_keys
306
+ @associations_keys ||= associations.keys
307
+ end
308
+
309
+ # For defining an "has many" association on a model. This defines a set of methods on
310
+ # your model instances. For instance, if you define the association on a Person model:
311
+ #
312
+ #
313
+ # .. code-block:: ruby
314
+ #
315
+ # has_many :out, :vehicles, type: :has_vehicle
316
+ #
317
+ # This would define the following methods:
318
+ #
319
+ # **#vehicles**
320
+ # Returns a QueryProxy object. This is an Enumerable object and thus can be iterated
321
+ # over. It also has the ability to accept class-level methods from the Vehicle model
322
+ # (including calls to association methods)
323
+ #
324
+ # **#vehicles=**
325
+ # Takes an array of Vehicle objects and replaces all current ``:HAS_VEHICLE`` relationships
326
+ # with new relationships refering to the specified objects
327
+ #
328
+ # **.vehicles**
329
+ # Returns a QueryProxy object. This would represent all ``Vehicle`` objects associated with
330
+ # either all ``Person`` nodes (if ``Person.vehicles`` is called), or all ``Vehicle`` objects
331
+ # associated with the ``Person`` nodes thus far represented in the QueryProxy chain.
332
+ # For example:
333
+ #
334
+ # .. code-block:: ruby
335
+ #
336
+ # company.people.where(age: 40).vehicles
337
+ #
338
+ # Arguments:
339
+ # **direction:**
340
+ # **Available values:** ``:in``, ``:out``, or ``:both``.
341
+ #
342
+ # Refers to the relative to the model on which the association is being defined.
343
+ #
344
+ # Example:
345
+ #
346
+ # .. code-block:: ruby
347
+ #
348
+ # Person.has_many :out, :posts, type: :wrote
349
+ #
350
+ # means that a `WROTE` relationship goes from a `Person` node to a `Post` node
351
+ #
352
+ # **name:**
353
+ # The name of the association. The affects the methods which are created (see above).
354
+ # The name is also used to form default assumptions about the model which is being referred to
355
+ #
356
+ # Example:
357
+ #
358
+ # .. code-block:: ruby
359
+ #
360
+ # Person.has_many :out, :posts, type: :wrote
361
+ #
362
+ # will assume a `model_class` option of ``'Post'`` unless otherwise specified
363
+ #
364
+ # **options:** A ``Hash`` of options. Allowed keys are:
365
+ # *type*: The Neo4j relationship type. This option is required unless either the
366
+ # `origin` or `rel_class` options are specified
367
+ #
368
+ # *origin*: The name of the association from another model which the `type` and `model_class`
369
+ # can be gathered.
370
+ #
371
+ # Example:
372
+ #
373
+ # .. code-block:: ruby
374
+ #
375
+ # # `model_class` of `Post` is assumed here
376
+ # Person.has_many :out, :posts, origin: :author
377
+ #
378
+ # Post.has_one :in, :author, type: :has_author, model_class: :Person
379
+ #
380
+ # *model_class*: The model class to which the association is referring. Can be a
381
+ # Symbol/String (or an ``Array`` of same) with the name of the `ActiveNode` class,
382
+ # `false` to specify any model, or nil to specify that it should be guessed.
383
+ #
384
+ # *rel_class*: The ``ActiveRel`` class to use for this association. Can be either a
385
+ # model object ``include`` ing ``ActiveRel`` or a Symbol/String (or an ``Array`` of same).
386
+ # **A Symbol or String is recommended** to avoid load-time issues
387
+ #
388
+ # *dependent*: Enables deletion cascading.
389
+ # **Available values:** ``:delete``, ``:delete_orphans``, ``:destroy``, ``:destroy_orphans``
390
+ # (note that the ``:destroy_orphans`` option is known to be "very metal". Caution advised)
391
+ #
392
+ def has_many(direction, name, options = {}) # rubocop:disable Naming/PredicateName
393
+ name = name.to_sym
394
+ build_association(:has_many, direction, name, options)
395
+
396
+ define_has_many_methods(name, options)
397
+ end
398
+
399
+ # For defining an "has one" association on a model. This defines a set of methods on
400
+ # your model instances. For instance, if you define the association on a Person model:
401
+ #
402
+ # has_one :out, :vehicle, type: :has_vehicle
403
+ #
404
+ # This would define the methods: ``#vehicle``, ``#vehicle=``, and ``.vehicle``.
405
+ #
406
+ # See :ref:`#has_many <Neo4j/ActiveNode/HasN/ClassMethods#has_many>` for anything
407
+ # not specified here
408
+ #
409
+ def has_one(direction, name, options = {}) # rubocop:disable Naming/PredicateName
410
+ name = name.to_sym
411
+ build_association(:has_one, direction, name, options)
412
+
413
+ define_has_one_methods(name, options)
414
+ end
415
+
416
+ private
417
+
418
+ def define_has_many_methods(name, association_options)
419
+ default_options = association_options.slice(:labels)
420
+
421
+ define_method(name) do |node = nil, rel = nil, options = {}|
422
+ # return [].freeze unless self._persisted_obj
423
+
424
+ options, node = node, nil if node.is_a?(Hash)
425
+
426
+ options = default_options.merge(options)
427
+
428
+ association_proxy(name, {node: node, rel: rel, source_object: self, labels: options[:labels]}.merge!(options))
429
+ end
430
+
431
+ define_has_many_setter(name)
432
+
433
+ define_has_many_id_methods(name)
434
+
435
+ define_class_method(name) do |node = nil, rel = nil, options = {}|
436
+ options, node = node, nil if node.is_a?(Hash)
437
+
438
+ options = default_options.merge(options)
439
+
440
+ association_proxy(name, {node: node, rel: rel, labels: options[:labels]}.merge!(options))
441
+ end
442
+ end
443
+
444
+ def define_has_many_setter(name)
445
+ define_method("#{name}=") do |other_nodes|
446
+ association_proxy_cache.clear
447
+
448
+ clear_deferred_nodes_for_association(name)
449
+
450
+ self.class.run_transaction { association_proxy(name).replace_with(other_nodes) }
451
+ end
452
+ end
453
+
454
+ def define_has_many_id_methods(name)
455
+ define_method_unless_defined("#{name.to_s.singularize}_ids") do
456
+ association_proxy(name).result_ids
457
+ end
458
+
459
+ define_method_unless_defined("#{name.to_s.singularize}_ids=") do |ids|
460
+ clear_deferred_nodes_for_association(name)
461
+ association_proxy(name).replace_with(Array(ids).reject(&:blank?))
462
+ end
463
+
464
+ define_method_unless_defined("#{name.to_s.singularize}_neo_ids") do
465
+ association_proxy(name).pluck(:neo_id)
466
+ end
467
+ end
468
+
469
+ def define_method_unless_defined(method_name, &block)
470
+ define_method(method_name, block) unless method_defined?(method_name)
471
+ end
472
+
473
+ def define_has_one_methods(name, association_options)
474
+ default_options = association_options.slice(:labels)
475
+
476
+ define_has_one_getter(name, default_options)
477
+
478
+ define_has_one_setter(name)
479
+
480
+ define_has_one_id_methods(name)
481
+
482
+ define_class_method(name) do |node = nil, rel = nil, options = {}|
483
+ options, node = node, nil if node.is_a?(Hash)
484
+
485
+ options = default_options.merge(options)
486
+
487
+ association_proxy(name, {node: node, rel: rel, labels: options[:labels]}.merge!(options))
488
+ end
489
+ end
490
+
491
+ def define_has_one_id_methods(name)
492
+ define_method_unless_defined("#{name}_id") do
493
+ association_proxy(name).result_ids.first
494
+ end
495
+
496
+ define_method_unless_defined("#{name}_id=") do |id|
497
+ association_proxy(name).replace_with(id)
498
+ end
499
+
500
+ define_method_unless_defined("#{name}_neo_id") do
501
+ association_proxy(name).pluck(:neo_id).first
502
+ end
503
+ end
504
+
505
+ def define_has_one_getter(name, default_options)
506
+ define_method(name) do |node = nil, rel = nil, options = {}|
507
+ options, node = node, nil if node.is_a?(Hash)
508
+
509
+ options = default_options.merge(options)
510
+
511
+ association_proxy = association_proxy(name, {node: node, rel: rel}.merge!(options))
512
+
513
+ # Return all results if options[:chainable] == true or a variable-length relationship length was given
514
+ if options[:chainable] || (options[:rel_length] && !options[:rel_length].is_a?(Integer))
515
+ association_proxy
516
+ else
517
+ o = association_proxy.result.first
518
+ self.class.send(:association_target_class, name).try(:nodeify, o) || o
519
+ end
520
+ end
521
+ end
522
+
523
+ def define_has_one_setter(name)
524
+ define_method("#{name}=") do |other_node|
525
+ if persisted?
526
+ other_node.save if other_node.respond_to?(:persisted?) && !other_node.persisted?
527
+ association_proxy_cache.clear # TODO: Should probably just clear for this association...
528
+ self.class.run_transaction { association_proxy(name).replace_with(other_node) }
529
+ # handle_non_persisted_node(other_node)
530
+ else
531
+ defer_create(name, other_node, clear: true)
532
+ other_node
533
+ end
534
+ end
535
+ end
536
+
537
+ def define_class_method(*args, &block)
538
+ klass = class << self; self; end
539
+ klass.instance_eval do
540
+ define_method(*args, &block)
541
+ end
542
+ end
543
+
544
+ def association_query_proxy(name, options = {})
545
+ previous_query_proxy = options[:previous_query_proxy] || current_scope
546
+ query_proxy = previous_query_proxy || default_association_query_proxy
547
+ Neo4j::ActiveNode::Query::QueryProxy.new(association_target_class(name),
548
+ associations[name],
549
+ {session: neo4j_session,
550
+ query_proxy: query_proxy,
551
+ context: "#{query_proxy.context || self.name}##{name}",
552
+ optional: query_proxy.optional?,
553
+ association_labels: options[:labels],
554
+ source_object: query_proxy.source_object}.merge!(options)).tap do |query_proxy_result|
555
+ target_classes = association_target_classes(name)
556
+ return query_proxy_result.as_models(target_classes) if target_classes
557
+ end
558
+ end
559
+
560
+ def association_proxy(name, options = {})
561
+ AssociationProxy.new(association_query_proxy(name, options))
562
+ end
563
+
564
+ def association_target_class(name)
565
+ target_classes_or_nil = associations[name].target_classes_or_nil
566
+
567
+ return if !target_classes_or_nil.is_a?(Array) || target_classes_or_nil.size != 1
568
+
569
+ target_classes_or_nil[0]
570
+ end
571
+
572
+ def association_target_classes(name)
573
+ target_classes_or_nil = associations[name].target_classes_or_nil
574
+
575
+ return if !target_classes_or_nil.is_a?(Array) || target_classes_or_nil.size <= 1
576
+
577
+ target_classes_or_nil
578
+ end
579
+
580
+ def default_association_query_proxy
581
+ Neo4j::ActiveNode::Query::QueryProxy.new("::#{self.name}".constantize, nil,
582
+ session: neo4j_session, query_proxy: nil, context: self.name.to_s)
583
+ end
584
+
585
+ def build_association(macro, direction, name, options)
586
+ options[:model_class] = options[:model_class].name if options[:model_class] == self
587
+ Neo4j::ActiveNode::HasN::Association.new(macro, direction, name, options).tap do |association|
588
+ add_association(name, association)
589
+ create_reflection(macro, name, association, self)
590
+ end
591
+
592
+ @associations_keys = nil
593
+
594
+ # Re-raise any exception with added class name and association name to
595
+ # make sure error message is helpful
596
+ rescue StandardError => e
597
+ raise e.class, "#{e.message} (#{self.class}##{name})"
598
+ end
599
+
600
+ def add_association(name, association_object)
601
+ fail "Association `#{name}` defined for a second time. "\
602
+ 'Associations can only be defined once' if duplicate_association?(name)
603
+ associations[name] = association_object
604
+ end
605
+
606
+ def duplicate_association?(name)
607
+ associations.key?(name) && parent_associations[name] != associations[name]
608
+ end
609
+ end
610
+ # rubocop:enable Metrics/ModuleLength
611
+ end
612
+ end