activegraph 10.0.0.pre.alpha.6

Sign up to get free protection for your applications and to get access to all the features.
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