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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2016 -0
- data/CONTRIBUTORS +12 -0
- data/Gemfile +24 -0
- data/README.md +111 -0
- data/activegraph.gemspec +52 -0
- data/bin/rake +17 -0
- data/config/locales/en.yml +5 -0
- data/config/neo4j/add_classnames.yml +1 -0
- data/config/neo4j/config.yml +35 -0
- data/lib/active_graph.rb +123 -0
- data/lib/active_graph/ansi.rb +14 -0
- data/lib/active_graph/attribute_set.rb +32 -0
- data/lib/active_graph/base.rb +77 -0
- data/lib/active_graph/class_arguments.rb +39 -0
- data/lib/active_graph/config.rb +135 -0
- data/lib/active_graph/core.rb +14 -0
- data/lib/active_graph/core/connection_failed_error.rb +6 -0
- data/lib/active_graph/core/cypher_error.rb +37 -0
- data/lib/active_graph/core/entity.rb +11 -0
- data/lib/active_graph/core/instrumentable.rb +37 -0
- data/lib/active_graph/core/label.rb +135 -0
- data/lib/active_graph/core/logging.rb +44 -0
- data/lib/active_graph/core/node.rb +15 -0
- data/lib/active_graph/core/querable.rb +41 -0
- data/lib/active_graph/core/query.rb +485 -0
- data/lib/active_graph/core/query_builder.rb +18 -0
- data/lib/active_graph/core/query_clauses.rb +727 -0
- data/lib/active_graph/core/query_ext.rb +24 -0
- data/lib/active_graph/core/query_find_in_batches.rb +46 -0
- data/lib/active_graph/core/record.rb +51 -0
- data/lib/active_graph/core/result.rb +31 -0
- data/lib/active_graph/core/schema.rb +65 -0
- data/lib/active_graph/core/schema_errors.rb +12 -0
- data/lib/active_graph/core/wrappable.rb +30 -0
- data/lib/active_graph/errors.rb +59 -0
- data/lib/active_graph/lazy_attribute_hash.rb +38 -0
- data/lib/active_graph/migration.rb +148 -0
- data/lib/active_graph/migrations.rb +27 -0
- data/lib/active_graph/migrations/base.rb +77 -0
- data/lib/active_graph/migrations/check_pending.rb +20 -0
- data/lib/active_graph/migrations/helpers.rb +105 -0
- data/lib/active_graph/migrations/helpers/id_property.rb +72 -0
- data/lib/active_graph/migrations/helpers/relationships.rb +66 -0
- data/lib/active_graph/migrations/helpers/schema.rb +63 -0
- data/lib/active_graph/migrations/migration_file.rb +24 -0
- data/lib/active_graph/migrations/runner.rb +195 -0
- data/lib/active_graph/migrations/schema.rb +64 -0
- data/lib/active_graph/migrations/schema_migration.rb +14 -0
- data/lib/active_graph/model_schema.rb +139 -0
- data/lib/active_graph/node.rb +110 -0
- data/lib/active_graph/node/callbacks.rb +8 -0
- data/lib/active_graph/node/dependent.rb +11 -0
- data/lib/active_graph/node/dependent/association_methods.rb +49 -0
- data/lib/active_graph/node/dependent/query_proxy_methods.rb +52 -0
- data/lib/active_graph/node/dependent_callbacks.rb +31 -0
- data/lib/active_graph/node/enum.rb +26 -0
- data/lib/active_graph/node/has_n.rb +602 -0
- data/lib/active_graph/node/has_n/association.rb +278 -0
- data/lib/active_graph/node/has_n/association/rel_factory.rb +61 -0
- data/lib/active_graph/node/has_n/association/rel_wrapper.rb +23 -0
- data/lib/active_graph/node/has_n/association_cypher_methods.rb +108 -0
- data/lib/active_graph/node/id_property.rb +224 -0
- data/lib/active_graph/node/id_property/accessor.rb +62 -0
- data/lib/active_graph/node/initialize.rb +21 -0
- data/lib/active_graph/node/labels.rb +207 -0
- data/lib/active_graph/node/labels/index.rb +37 -0
- data/lib/active_graph/node/labels/reloading.rb +21 -0
- data/lib/active_graph/node/node_list_formatter.rb +13 -0
- data/lib/active_graph/node/node_wrapper.rb +54 -0
- data/lib/active_graph/node/orm_adapter.rb +82 -0
- data/lib/active_graph/node/persistence.rb +186 -0
- data/lib/active_graph/node/property.rb +60 -0
- data/lib/active_graph/node/query.rb +76 -0
- data/lib/active_graph/node/query/query_proxy.rb +367 -0
- data/lib/active_graph/node/query/query_proxy_eager_loading.rb +177 -0
- data/lib/active_graph/node/query/query_proxy_eager_loading/association_tree.rb +75 -0
- data/lib/active_graph/node/query/query_proxy_enumerable.rb +110 -0
- data/lib/active_graph/node/query/query_proxy_find_in_batches.rb +19 -0
- data/lib/active_graph/node/query/query_proxy_link.rb +139 -0
- data/lib/active_graph/node/query/query_proxy_methods.rb +303 -0
- data/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb +99 -0
- data/lib/active_graph/node/query_methods.rb +68 -0
- data/lib/active_graph/node/reflection.rb +86 -0
- data/lib/active_graph/node/rels.rb +11 -0
- data/lib/active_graph/node/scope.rb +166 -0
- data/lib/active_graph/node/unpersisted.rb +48 -0
- data/lib/active_graph/node/validations.rb +59 -0
- data/lib/active_graph/paginated.rb +27 -0
- data/lib/active_graph/railtie.rb +108 -0
- data/lib/active_graph/relationship.rb +68 -0
- data/lib/active_graph/relationship/callbacks.rb +21 -0
- data/lib/active_graph/relationship/initialize.rb +28 -0
- data/lib/active_graph/relationship/persistence.rb +133 -0
- data/lib/active_graph/relationship/persistence/query_factory.rb +95 -0
- data/lib/active_graph/relationship/property.rb +92 -0
- data/lib/active_graph/relationship/query.rb +99 -0
- data/lib/active_graph/relationship/rel_wrapper.rb +31 -0
- data/lib/active_graph/relationship/related_node.rb +87 -0
- data/lib/active_graph/relationship/types.rb +80 -0
- data/lib/active_graph/relationship/validations.rb +8 -0
- data/lib/active_graph/schema/operation.rb +102 -0
- data/lib/active_graph/shared.rb +48 -0
- data/lib/active_graph/shared/attributes.rb +217 -0
- data/lib/active_graph/shared/callbacks.rb +66 -0
- data/lib/active_graph/shared/cypher.rb +37 -0
- data/lib/active_graph/shared/declared_properties.rb +204 -0
- data/lib/active_graph/shared/declared_property.rb +109 -0
- data/lib/active_graph/shared/declared_property/index.rb +37 -0
- data/lib/active_graph/shared/enum.rb +167 -0
- data/lib/active_graph/shared/filtered_hash.rb +79 -0
- data/lib/active_graph/shared/identity.rb +34 -0
- data/lib/active_graph/shared/initialize.rb +65 -0
- data/lib/active_graph/shared/marshal.rb +23 -0
- data/lib/active_graph/shared/mass_assignment.rb +63 -0
- data/lib/active_graph/shared/permitted_attributes.rb +28 -0
- data/lib/active_graph/shared/persistence.rb +272 -0
- data/lib/active_graph/shared/property.rb +249 -0
- data/lib/active_graph/shared/query_factory.rb +122 -0
- data/lib/active_graph/shared/rel_type_converters.rb +43 -0
- data/lib/active_graph/shared/serialized_properties.rb +30 -0
- data/lib/active_graph/shared/type_converters.rb +439 -0
- data/lib/active_graph/shared/typecasted_attributes.rb +99 -0
- data/lib/active_graph/shared/typecaster.rb +53 -0
- data/lib/active_graph/shared/validations.rb +44 -0
- data/lib/active_graph/tasks/migration.rake +204 -0
- data/lib/active_graph/timestamps.rb +11 -0
- data/lib/active_graph/timestamps/created.rb +9 -0
- data/lib/active_graph/timestamps/updated.rb +9 -0
- data/lib/active_graph/transaction.rb +22 -0
- data/lib/active_graph/transactions.rb +57 -0
- data/lib/active_graph/type_converters.rb +7 -0
- data/lib/active_graph/undeclared_properties.rb +53 -0
- data/lib/active_graph/version.rb +3 -0
- data/lib/active_graph/wrapper.rb +4 -0
- data/lib/rails/generators/active_graph/migration/migration_generator.rb +16 -0
- data/lib/rails/generators/active_graph/migration/templates/migration.erb +9 -0
- data/lib/rails/generators/active_graph/model/model_generator.rb +89 -0
- data/lib/rails/generators/active_graph/model/templates/migration.erb +11 -0
- data/lib/rails/generators/active_graph/model/templates/model.erb +15 -0
- data/lib/rails/generators/active_graph/upgrade_v8/templates/migration.erb +17 -0
- data/lib/rails/generators/active_graph/upgrade_v8/upgrade_v8_generator.rb +34 -0
- data/lib/rails/generators/active_graph_generator.rb +121 -0
- metadata +423 -0
|
@@ -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
|