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,95 @@
|
|
|
1
|
+
module ActiveGraph::Relationship::Persistence
|
|
2
|
+
# This class builds and executes a Cypher query, using information from the graph objects to determine
|
|
3
|
+
# whether they need to be created simultaneously.
|
|
4
|
+
# It keeps the rel instance from being responsible for inspecting the nodes or talking with Shared::QueryFactory.
|
|
5
|
+
class QueryFactory
|
|
6
|
+
NODE_SYMBOLS = [:from_node, :to_node]
|
|
7
|
+
attr_reader :from_node, :to_node, :rel, :unwrapped_rel
|
|
8
|
+
|
|
9
|
+
def initialize(from_node, to_node, rel)
|
|
10
|
+
@from_node = from_node
|
|
11
|
+
@to_node = to_node
|
|
12
|
+
@rel = rel
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# TODO: This feels like it should also wrap the rel, but that is handled in ActiveGraph::Relationship::Persistence at the moment.
|
|
16
|
+
# Builds and executes the query using the objects giving during init.
|
|
17
|
+
# It holds the process:
|
|
18
|
+
# * Execute node callbacks if needed
|
|
19
|
+
# * Create and execute the query
|
|
20
|
+
# * Mix the query response into the unpersisted objects given during init
|
|
21
|
+
def build!
|
|
22
|
+
node_before_callbacks! do
|
|
23
|
+
res = query_factory(rel, rel_id, iterative_query).query.unwrapped.return(*unpersisted_return_ids).first
|
|
24
|
+
node_symbols.each { |n| wrap!(send(n), res, n) }
|
|
25
|
+
@unwrapped_rel = res[rel_id]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def rel_id
|
|
32
|
+
@rel_id ||= rel.rel_identifier
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Node callbacks only need to be executed if the node is not persisted. We let the `conditional_callback` method do the work,
|
|
36
|
+
# we only have to give it the type of callback we expect to be run and the condition which, if true, will prevent it from executing.
|
|
37
|
+
def node_before_callbacks!
|
|
38
|
+
validate_unpersisted_nodes!
|
|
39
|
+
from_node.conditional_callback(:create, from_node.persisted?) do
|
|
40
|
+
to_node.conditional_callback(:create, to_node.persisted?) do
|
|
41
|
+
yield
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_unpersisted_nodes!
|
|
47
|
+
node_symbols.each do |s|
|
|
48
|
+
obj = send(s)
|
|
49
|
+
next if obj.persisted?
|
|
50
|
+
fail RelCreateFailedError, "Cannot create rel with unpersisted, invalid #{s}" unless obj.valid?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Each node must be either created or matched before the relationship can be created. This class does not know or care about
|
|
55
|
+
# how that happens, it just knows that it needs a usable ActiveGraph::Core::Query object to do that.
|
|
56
|
+
# This method is "iterative" because it creates one factory for each node but the second builds upon the first.
|
|
57
|
+
def iterative_query
|
|
58
|
+
node_symbols.inject(false) do |iterative_query, sym|
|
|
59
|
+
obj = send(sym)
|
|
60
|
+
query_factory(obj, sym, iterative_query)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Isolates the dependency to the shared class. This has an awareness of ActiveGraph::Core::Query and will match or create
|
|
65
|
+
# based on the current state of the object passed in.
|
|
66
|
+
def query_factory(obj, sym, factory = false)
|
|
67
|
+
ActiveGraph::Shared::QueryFactory.create(obj, sym).tap do |factory_instance|
|
|
68
|
+
factory_instance.base_query = factory.blank? ? false : factory.query
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [Array<Symbol>] The Cypher identifiers that will be returned from the query.
|
|
73
|
+
# We only need to return objects from our query that were created during it, otherwise we impact performance.
|
|
74
|
+
def unpersisted_return_ids
|
|
75
|
+
[rel_id].tap do |result|
|
|
76
|
+
node_symbols.each { |k| result << k unless send(k).persisted? }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param [ActiveGraph::Node] node A node, persisted or unpersisted
|
|
81
|
+
# @param [Struct] res The result of calling `return` on a ActiveGraph::Core::Query object. It responds to the same keys
|
|
82
|
+
# as our graph objects. If the object is unpersisted and was created during the query, the unwrapped node is mixed
|
|
83
|
+
# in, making the object reflect as "persisted".
|
|
84
|
+
# @param [Symbol] key :from_node or :to_node, the object to request from the response.
|
|
85
|
+
def wrap!(node, res, key)
|
|
86
|
+
return if node.persisted? || !res.keys.include?(key)
|
|
87
|
+
unwrapped = res[key]
|
|
88
|
+
node.init_on_load(unwrapped, unwrapped.properties)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def node_symbols
|
|
92
|
+
self.class::NODE_SYMBOLS
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require 'active_graph/class_arguments'
|
|
2
|
+
|
|
3
|
+
module ActiveGraph::Relationship
|
|
4
|
+
module Property
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
include ActiveGraph::Shared::Property
|
|
7
|
+
|
|
8
|
+
%w(to_node from_node).each do |direction|
|
|
9
|
+
define_method(direction.to_s) { instance_variable_get("@#{direction}") }
|
|
10
|
+
define_method("#{direction}=") do |argument|
|
|
11
|
+
fail FrozenRelError, 'Relationship start/end nodes cannot be changed once persisted' if _persisted_obj
|
|
12
|
+
instance_variable_set("@#{direction}", argument)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
alias start_node from_node
|
|
17
|
+
alias end_node to_node
|
|
18
|
+
|
|
19
|
+
%w(start_node end_node).each do |direction|
|
|
20
|
+
define_method("#{direction}_id") { send(direction).neo_id if direction }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String] a string representing the relationship type that will be created
|
|
24
|
+
def type
|
|
25
|
+
self.class.type
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(attributes = nil)
|
|
29
|
+
super(attributes)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def creates_unique_option
|
|
33
|
+
self.class.creates_unique_option
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module ClassMethods
|
|
37
|
+
include ActiveGraph::Shared::Cypher::CreateMethod
|
|
38
|
+
|
|
39
|
+
# Extracts keys from attributes hash which are relationships of the model
|
|
40
|
+
# TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save?
|
|
41
|
+
def extract_association_attributes!(attributes)
|
|
42
|
+
return if attributes.blank?
|
|
43
|
+
{}.tap do |relationship_props|
|
|
44
|
+
attributes.each_key do |key|
|
|
45
|
+
relationship_props[key] = attributes.delete(key) if [:from_node, :to_node].include?(key)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def id_property_name
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
%w(to_class from_class).each do |direction|
|
|
55
|
+
define_method(direction.to_s) do |argument = nil|
|
|
56
|
+
if !argument.nil?
|
|
57
|
+
ActiveGraph::ClassArguments.validate_argument!(argument, direction)
|
|
58
|
+
|
|
59
|
+
instance_variable_set("@#{direction}", argument)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
self.instance_variable_get("@#{direction}")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
define_method("_#{direction}") { instance_variable_get "@#{direction}" }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def valid_class_argument?(class_argument)
|
|
69
|
+
[String, Symbol, FalseClass].include?(class_argument.class) ||
|
|
70
|
+
(class_argument.is_a?(Array) && class_argument.all? { |c| [String, Symbol].include?(c.class) })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
alias start_class from_class
|
|
74
|
+
alias end_class to_class
|
|
75
|
+
|
|
76
|
+
def load_entity(id)
|
|
77
|
+
ActiveGraph::Node.load(id)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def load_nodes(from_node = nil, to_node = nil)
|
|
84
|
+
@from_node = RelatedNode.new(from_node)
|
|
85
|
+
@to_node = RelatedNode.new(to_node)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def inspect_attributes
|
|
89
|
+
attributes.to_a
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module ActiveGraph::Relationship
|
|
2
|
+
module Query
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
class RecordNotFound < ActiveGraph::RecordNotFound; end
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
# Returns the object with the specified neo4j id.
|
|
9
|
+
# @param [String,Integer] id of node to find
|
|
10
|
+
def find(id)
|
|
11
|
+
fail "Unknown argument #{id.class} in find method (expected String or Integer)" if !(id.is_a?(String) || id.is_a?(Integer))
|
|
12
|
+
find_by_id(id)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Loads the relationship using its neo_id.
|
|
16
|
+
def find_by_id(key)
|
|
17
|
+
query = ActiveGraph::Base.new_query
|
|
18
|
+
result = query.match('()-[r]-()').where('ID(r)' => key.to_i).limit(1).return(:r).first
|
|
19
|
+
fail RecordNotFound.new("Couldn't find #{name} with 'id'=#{key.inspect}", name, key) if result.blank?
|
|
20
|
+
result[:r]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Performs a very basic match on the relationship.
|
|
24
|
+
# This is not executed lazily, it will immediately return matching objects.
|
|
25
|
+
# To use a string, prefix the property with "r1"
|
|
26
|
+
# @example Match with a string
|
|
27
|
+
# MyRelClass.where('r1.grade > r1')
|
|
28
|
+
def where(args = {})
|
|
29
|
+
where_query.where(where_string(args)).pluck(:r1)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Performs a basic match on the relationship, returning all results.
|
|
33
|
+
# This is not executed lazily, it will immediately return matching objects.
|
|
34
|
+
def all
|
|
35
|
+
all_query.pluck(:r1)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def first
|
|
39
|
+
all_query.limit(1).order('ID(r1)').pluck(:r1).first
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def last
|
|
43
|
+
all_query.limit(1).order('ID(r1) DESC').pluck(:r1).first
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def deprecation_warning!
|
|
49
|
+
ActiveSupport::Deprecation.warn 'The ActiveGraph::Relationship::Query module has been deprecated and will be removed in a future version of the gem.', caller
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def where_query
|
|
53
|
+
deprecation_warning!
|
|
54
|
+
ActiveGraph::Base.new_query.match("#{cypher_string(:outbound)}-[r1:`#{type}`]->#{cypher_string(:inbound)}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def all_query
|
|
58
|
+
deprecation_warning!
|
|
59
|
+
ActiveGraph::Base.new_query.match("#{cypher_string}-[r1:`#{type}`]->#{cypher_string(:inbound)}")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def cypher_string(dir = :outbound)
|
|
63
|
+
case dir
|
|
64
|
+
when :outbound
|
|
65
|
+
identifier = '(n1'
|
|
66
|
+
identifier + (_from_class == :any ? ')' : cypher_label(:outbound))
|
|
67
|
+
when :inbound
|
|
68
|
+
identifier = '(n2'
|
|
69
|
+
identifier + (_to_class == :any ? ')' : cypher_label(:inbound))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def cypher_label(dir = :outbound)
|
|
74
|
+
target_class = dir == :outbound ? as_constant(_from_class) : as_constant(_to_class)
|
|
75
|
+
":`#{target_class.mapped_label_name}`)"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def as_constant(given_class)
|
|
79
|
+
case given_class
|
|
80
|
+
when String, Symbol
|
|
81
|
+
given_class.to_s.constantize
|
|
82
|
+
when Array
|
|
83
|
+
fail "Relationship query methods are being deprecated and do not support Array (from|to)_class options. Current value: #{given_class}"
|
|
84
|
+
else
|
|
85
|
+
given_class
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def where_string(args)
|
|
90
|
+
case args
|
|
91
|
+
when Hash
|
|
92
|
+
args.map { |k, v| v.is_a?(Integer) ? "r1.#{k} = #{v}" : "r1.#{k} = '#{v}'" }.join(', ')
|
|
93
|
+
else
|
|
94
|
+
args
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
wrapping_proc = proc do |relationship|
|
|
4
|
+
ActiveGraph::RelWrapping.wrapper(relationship)
|
|
5
|
+
end
|
|
6
|
+
Neo4j::Driver::Types::Relationship.wrapper_callback(wrapping_proc)
|
|
7
|
+
|
|
8
|
+
module ActiveGraph
|
|
9
|
+
module RelWrapping
|
|
10
|
+
class << self
|
|
11
|
+
def wrapper(rel)
|
|
12
|
+
rel.properties.symbolize_keys!
|
|
13
|
+
begin
|
|
14
|
+
most_concrete_class = class_from_type(rel.type).constantize
|
|
15
|
+
return rel unless most_concrete_class < ActiveGraph::Relationship
|
|
16
|
+
most_concrete_class.new
|
|
17
|
+
rescue NameError => e
|
|
18
|
+
raise e unless e.message =~ /(uninitialized|wrong) constant/
|
|
19
|
+
|
|
20
|
+
return rel
|
|
21
|
+
end.tap do |wrapped_rel|
|
|
22
|
+
wrapped_rel.init_on_load(rel, rel.start_node_id, rel.end_node_id, rel.type)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def class_from_type(type)
|
|
27
|
+
ActiveGraph::Relationship::Types::WRAPPED_CLASSES[type] || ActiveGraph::Relationship::Types::WRAPPED_CLASSES[type] = type.to_s.downcase.camelize
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module ActiveGraph::Relationship
|
|
2
|
+
# A container for Relationship's :inbound and :outbound methods. It provides lazy loading of nodes.
|
|
3
|
+
# It's important (or maybe not really IMPORTANT, but at least worth mentioning) that calling method_missing
|
|
4
|
+
# will result in a query to load the node if the node is not already loaded.
|
|
5
|
+
class RelatedNode
|
|
6
|
+
class UnsetRelatedNodeError < ActiveGraph::Error; end
|
|
7
|
+
|
|
8
|
+
# Relationship's related nodes can be initialized with nothing, an integer, or a fully wrapped node.
|
|
9
|
+
#
|
|
10
|
+
# Initialization with nothing happens when a new, non-persisted Relationship object is first initialized.
|
|
11
|
+
#
|
|
12
|
+
# Initialization with an integer happens when a relationship is loaded from the database. It loads using the ID
|
|
13
|
+
# because that is provided by the Cypher response and does not require an extra query.
|
|
14
|
+
def initialize(node = nil)
|
|
15
|
+
@node = valid_node_param?(node) ? node : (fail ActiveGraph::InvalidParameterError, 'RelatedNode must be initialized with either a node ID or node')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Loads the node if needed, then conducts comparison.
|
|
19
|
+
def ==(other)
|
|
20
|
+
loaded if @node.is_a?(Integer)
|
|
21
|
+
@node == other
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns the neo_id of a given node without loading.
|
|
25
|
+
def neo_id
|
|
26
|
+
loaded? ? @node.neo_id : @node
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Loads a node from the database or returns the node if already laoded
|
|
30
|
+
def loaded
|
|
31
|
+
fail UnsetRelatedNodeError, 'Node not set, cannot load' if @node.nil?
|
|
32
|
+
@node = if @node.respond_to?(:neo_id)
|
|
33
|
+
@node
|
|
34
|
+
else
|
|
35
|
+
ActiveGraph::Base.new_query.match(:n).where(n: {neo_id: @node}).pluck(:n).first
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param [String, Symbol, Array] clazz An alternate label to use in the event the node is not present or loaded
|
|
40
|
+
def cypher_representation(clazz)
|
|
41
|
+
case
|
|
42
|
+
when !set?
|
|
43
|
+
"(#{formatted_label_list(clazz)})"
|
|
44
|
+
when set? && !loaded?
|
|
45
|
+
"(Node with neo_id #{@node})"
|
|
46
|
+
else
|
|
47
|
+
node_class = self.class
|
|
48
|
+
id_name = node_class.id_property_name
|
|
49
|
+
labels = ':' + node_class.mapped_label_names.join(':')
|
|
50
|
+
|
|
51
|
+
"(#{labels} {#{id_name}: #{@node.id.inspect}})"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Boolean] indicates whether a node has or has not been fully loaded from the database
|
|
56
|
+
def loaded?
|
|
57
|
+
@node.respond_to?(:neo_id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def set?
|
|
61
|
+
!@node.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def method_missing(*args, &block)
|
|
65
|
+
loaded.send(*args, &block)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
69
|
+
loaded if @node.is_a?(Numeric)
|
|
70
|
+
@node.respond_to?(method_name) ? true : super
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def class
|
|
74
|
+
loaded.send(:class)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def formatted_label_list(list)
|
|
80
|
+
list.is_a?(Array) ? list.join(' || ') : list
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def valid_node_param?(node)
|
|
84
|
+
node.nil? || node.is_a?(Integer) || node.respond_to?(:neo_id)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module ActiveGraph
|
|
2
|
+
module Relationship
|
|
3
|
+
# provides mapping of type to model name
|
|
4
|
+
module Types
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
# WRAPPED_CLASSES maps relationship types to Relationship models.
|
|
8
|
+
#
|
|
9
|
+
# Typically, it's a 1:1 relationship, with a type having a model of the same name. Sometimes, someone needs to be a precious
|
|
10
|
+
# snowflake and have a model name that doesn't match the rel type, so this comes in handy.
|
|
11
|
+
#
|
|
12
|
+
# As an example, Chris often finds it easier to name models after the classes that use the relationship: `StudentLesson` instead of
|
|
13
|
+
# `EnrolledIn`, because it's easier to remember "A student has a relationship to lesson" than "the type of relationship between Student
|
|
14
|
+
# and Lesson is 'EnrolledIn'." After all, that is a big part of why we have models, right? To make our lives easier?
|
|
15
|
+
#
|
|
16
|
+
# A model is added to WRAPPED_CLASSES when it is initalized AND when the `type` class method is called within a model. This means that
|
|
17
|
+
# it's possible a model will be added twice: once with the type version of the model name, again with the custom type. deal_with_it.gif.
|
|
18
|
+
WRAPPED_CLASSES = {}
|
|
19
|
+
|
|
20
|
+
included do
|
|
21
|
+
type self.namespaced_model_name, true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module ClassMethods
|
|
25
|
+
include ActiveGraph::Shared::RelTypeConverters
|
|
26
|
+
|
|
27
|
+
def inherited(subclass)
|
|
28
|
+
subclass.type subclass.namespaced_model_name, true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# When called without arguments, it will return the current setting or supply a default.
|
|
32
|
+
# When called with arguments, it will change the current setting.
|
|
33
|
+
# @param [String] given_type sets the relationship type when creating relationships via this class
|
|
34
|
+
# @param [Boolean] auto Should the given_type be changed in compliance with the gem's rel decorator setting?
|
|
35
|
+
def type(given_type = nil, auto = false)
|
|
36
|
+
case
|
|
37
|
+
when !given_type && type?
|
|
38
|
+
@type
|
|
39
|
+
when given_type
|
|
40
|
+
assign_type!(given_type, auto)
|
|
41
|
+
else
|
|
42
|
+
assign_type!(namespaced_model_name, true)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def namespaced_model_name
|
|
47
|
+
case ActiveGraph::Config[:module_handling]
|
|
48
|
+
when :demodulize
|
|
49
|
+
self.name.demodulize
|
|
50
|
+
when Proc
|
|
51
|
+
ActiveGraph::Config[:module_handling].call(self.name)
|
|
52
|
+
else
|
|
53
|
+
self.name
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def _wrapped_classes
|
|
58
|
+
WRAPPED_CLASSES
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_wrapped_class(type)
|
|
62
|
+
# WRAPPED_CLASSES[type.to_sym.downcase] = self.name
|
|
63
|
+
_wrapped_classes[type.to_sym] = self.name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def type?
|
|
67
|
+
!!@type
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def assign_type!(given_type, auto)
|
|
73
|
+
@type = (auto ? decorated_rel_type(given_type) : given_type).tap do |type|
|
|
74
|
+
add_wrapped_class(type)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|