activegraph 11.0.0.beta.1-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (144) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2016 -0
  3. data/CONTRIBUTORS +12 -0
  4. data/Gemfile +24 -0
  5. data/README.md +111 -0
  6. data/activegraph.gemspec +52 -0
  7. data/bin/rake +17 -0
  8. data/config/locales/en.yml +5 -0
  9. data/config/neo4j/add_classnames.yml +1 -0
  10. data/config/neo4j/config.yml +35 -0
  11. data/lib/active_graph.rb +123 -0
  12. data/lib/active_graph/ansi.rb +14 -0
  13. data/lib/active_graph/attribute_set.rb +32 -0
  14. data/lib/active_graph/base.rb +77 -0
  15. data/lib/active_graph/class_arguments.rb +39 -0
  16. data/lib/active_graph/config.rb +135 -0
  17. data/lib/active_graph/core.rb +14 -0
  18. data/lib/active_graph/core/connection_failed_error.rb +6 -0
  19. data/lib/active_graph/core/cypher_error.rb +37 -0
  20. data/lib/active_graph/core/entity.rb +11 -0
  21. data/lib/active_graph/core/instrumentable.rb +37 -0
  22. data/lib/active_graph/core/label.rb +135 -0
  23. data/lib/active_graph/core/logging.rb +44 -0
  24. data/lib/active_graph/core/node.rb +15 -0
  25. data/lib/active_graph/core/querable.rb +41 -0
  26. data/lib/active_graph/core/query.rb +485 -0
  27. data/lib/active_graph/core/query_builder.rb +18 -0
  28. data/lib/active_graph/core/query_clauses.rb +727 -0
  29. data/lib/active_graph/core/query_ext.rb +24 -0
  30. data/lib/active_graph/core/query_find_in_batches.rb +46 -0
  31. data/lib/active_graph/core/record.rb +51 -0
  32. data/lib/active_graph/core/result.rb +31 -0
  33. data/lib/active_graph/core/schema.rb +65 -0
  34. data/lib/active_graph/core/schema_errors.rb +12 -0
  35. data/lib/active_graph/core/wrappable.rb +30 -0
  36. data/lib/active_graph/errors.rb +59 -0
  37. data/lib/active_graph/lazy_attribute_hash.rb +38 -0
  38. data/lib/active_graph/migration.rb +148 -0
  39. data/lib/active_graph/migrations.rb +27 -0
  40. data/lib/active_graph/migrations/base.rb +77 -0
  41. data/lib/active_graph/migrations/check_pending.rb +20 -0
  42. data/lib/active_graph/migrations/helpers.rb +105 -0
  43. data/lib/active_graph/migrations/helpers/id_property.rb +72 -0
  44. data/lib/active_graph/migrations/helpers/relationships.rb +66 -0
  45. data/lib/active_graph/migrations/helpers/schema.rb +63 -0
  46. data/lib/active_graph/migrations/migration_file.rb +24 -0
  47. data/lib/active_graph/migrations/runner.rb +195 -0
  48. data/lib/active_graph/migrations/schema.rb +64 -0
  49. data/lib/active_graph/migrations/schema_migration.rb +14 -0
  50. data/lib/active_graph/model_schema.rb +139 -0
  51. data/lib/active_graph/node.rb +110 -0
  52. data/lib/active_graph/node/callbacks.rb +8 -0
  53. data/lib/active_graph/node/dependent.rb +11 -0
  54. data/lib/active_graph/node/dependent/association_methods.rb +49 -0
  55. data/lib/active_graph/node/dependent/query_proxy_methods.rb +52 -0
  56. data/lib/active_graph/node/dependent_callbacks.rb +31 -0
  57. data/lib/active_graph/node/enum.rb +26 -0
  58. data/lib/active_graph/node/has_n.rb +602 -0
  59. data/lib/active_graph/node/has_n/association.rb +278 -0
  60. data/lib/active_graph/node/has_n/association/rel_factory.rb +61 -0
  61. data/lib/active_graph/node/has_n/association/rel_wrapper.rb +23 -0
  62. data/lib/active_graph/node/has_n/association_cypher_methods.rb +108 -0
  63. data/lib/active_graph/node/id_property.rb +224 -0
  64. data/lib/active_graph/node/id_property/accessor.rb +62 -0
  65. data/lib/active_graph/node/initialize.rb +21 -0
  66. data/lib/active_graph/node/labels.rb +207 -0
  67. data/lib/active_graph/node/labels/index.rb +37 -0
  68. data/lib/active_graph/node/labels/reloading.rb +21 -0
  69. data/lib/active_graph/node/node_list_formatter.rb +13 -0
  70. data/lib/active_graph/node/node_wrapper.rb +54 -0
  71. data/lib/active_graph/node/orm_adapter.rb +82 -0
  72. data/lib/active_graph/node/persistence.rb +186 -0
  73. data/lib/active_graph/node/property.rb +60 -0
  74. data/lib/active_graph/node/query.rb +76 -0
  75. data/lib/active_graph/node/query/query_proxy.rb +367 -0
  76. data/lib/active_graph/node/query/query_proxy_eager_loading.rb +177 -0
  77. data/lib/active_graph/node/query/query_proxy_eager_loading/association_tree.rb +75 -0
  78. data/lib/active_graph/node/query/query_proxy_enumerable.rb +110 -0
  79. data/lib/active_graph/node/query/query_proxy_find_in_batches.rb +19 -0
  80. data/lib/active_graph/node/query/query_proxy_link.rb +139 -0
  81. data/lib/active_graph/node/query/query_proxy_methods.rb +303 -0
  82. data/lib/active_graph/node/query/query_proxy_methods_of_mass_updating.rb +99 -0
  83. data/lib/active_graph/node/query_methods.rb +68 -0
  84. data/lib/active_graph/node/reflection.rb +86 -0
  85. data/lib/active_graph/node/rels.rb +11 -0
  86. data/lib/active_graph/node/scope.rb +166 -0
  87. data/lib/active_graph/node/unpersisted.rb +48 -0
  88. data/lib/active_graph/node/validations.rb +59 -0
  89. data/lib/active_graph/paginated.rb +27 -0
  90. data/lib/active_graph/railtie.rb +108 -0
  91. data/lib/active_graph/relationship.rb +68 -0
  92. data/lib/active_graph/relationship/callbacks.rb +21 -0
  93. data/lib/active_graph/relationship/initialize.rb +28 -0
  94. data/lib/active_graph/relationship/persistence.rb +133 -0
  95. data/lib/active_graph/relationship/persistence/query_factory.rb +95 -0
  96. data/lib/active_graph/relationship/property.rb +92 -0
  97. data/lib/active_graph/relationship/query.rb +99 -0
  98. data/lib/active_graph/relationship/rel_wrapper.rb +31 -0
  99. data/lib/active_graph/relationship/related_node.rb +87 -0
  100. data/lib/active_graph/relationship/types.rb +80 -0
  101. data/lib/active_graph/relationship/validations.rb +8 -0
  102. data/lib/active_graph/schema/operation.rb +102 -0
  103. data/lib/active_graph/shared.rb +48 -0
  104. data/lib/active_graph/shared/attributes.rb +217 -0
  105. data/lib/active_graph/shared/callbacks.rb +66 -0
  106. data/lib/active_graph/shared/cypher.rb +37 -0
  107. data/lib/active_graph/shared/declared_properties.rb +204 -0
  108. data/lib/active_graph/shared/declared_property.rb +109 -0
  109. data/lib/active_graph/shared/declared_property/index.rb +37 -0
  110. data/lib/active_graph/shared/enum.rb +167 -0
  111. data/lib/active_graph/shared/filtered_hash.rb +79 -0
  112. data/lib/active_graph/shared/identity.rb +34 -0
  113. data/lib/active_graph/shared/initialize.rb +65 -0
  114. data/lib/active_graph/shared/marshal.rb +23 -0
  115. data/lib/active_graph/shared/mass_assignment.rb +63 -0
  116. data/lib/active_graph/shared/permitted_attributes.rb +28 -0
  117. data/lib/active_graph/shared/persistence.rb +272 -0
  118. data/lib/active_graph/shared/property.rb +249 -0
  119. data/lib/active_graph/shared/query_factory.rb +122 -0
  120. data/lib/active_graph/shared/rel_type_converters.rb +43 -0
  121. data/lib/active_graph/shared/serialized_properties.rb +30 -0
  122. data/lib/active_graph/shared/type_converters.rb +439 -0
  123. data/lib/active_graph/shared/typecasted_attributes.rb +99 -0
  124. data/lib/active_graph/shared/typecaster.rb +53 -0
  125. data/lib/active_graph/shared/validations.rb +44 -0
  126. data/lib/active_graph/tasks/migration.rake +204 -0
  127. data/lib/active_graph/timestamps.rb +11 -0
  128. data/lib/active_graph/timestamps/created.rb +9 -0
  129. data/lib/active_graph/timestamps/updated.rb +9 -0
  130. data/lib/active_graph/transaction.rb +22 -0
  131. data/lib/active_graph/transactions.rb +57 -0
  132. data/lib/active_graph/type_converters.rb +7 -0
  133. data/lib/active_graph/undeclared_properties.rb +53 -0
  134. data/lib/active_graph/version.rb +3 -0
  135. data/lib/active_graph/wrapper.rb +4 -0
  136. data/lib/rails/generators/active_graph/migration/migration_generator.rb +16 -0
  137. data/lib/rails/generators/active_graph/migration/templates/migration.erb +9 -0
  138. data/lib/rails/generators/active_graph/model/model_generator.rb +89 -0
  139. data/lib/rails/generators/active_graph/model/templates/migration.erb +11 -0
  140. data/lib/rails/generators/active_graph/model/templates/model.erb +15 -0
  141. data/lib/rails/generators/active_graph/upgrade_v8/templates/migration.erb +17 -0
  142. data/lib/rails/generators/active_graph/upgrade_v8/upgrade_v8_generator.rb +34 -0
  143. data/lib/rails/generators/active_graph_generator.rb +121 -0
  144. metadata +423 -0
@@ -0,0 +1,75 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Query
4
+ module QueryProxyEagerLoading
5
+ class AssociationTree < Hash
6
+ attr_accessor :model, :name, :association, :path, :rel_length
7
+
8
+ def initialize(model, name = nil, rel_length = nil)
9
+ super()
10
+ self.model = name ? target_class(model, name) : model
11
+ self.name = name
12
+ self.association = name ? model.associations[name] : nil
13
+ self.rel_length = rel_length
14
+ end
15
+
16
+ def clone
17
+ super.tap { |copy| copy.each { |key, value| copy[key] = value.clone } }
18
+ end
19
+
20
+ def add_spec(spec)
21
+ fail_spec(spec) unless model
22
+
23
+ case spec
24
+ when nil
25
+ nil
26
+ when Array
27
+ spec.each(&method(:add_spec))
28
+ when Hash
29
+ process_hash(spec)
30
+ when String
31
+ process_string(spec)
32
+ else
33
+ add_key(spec)
34
+ end
35
+ end
36
+
37
+ def fail_spec(spec)
38
+ fail "Cannot eager load \"past\" a polymorphic association. \
39
+ (Since the association can return multiple models, we don't how to handle the \"#{spec}\" association.)"
40
+ end
41
+
42
+ def paths(*prefix)
43
+ values.flat_map { |v| [[*prefix, v]] + v.paths(*prefix, v) }
44
+ end
45
+
46
+ def process_hash(spec)
47
+ spec.each { |key, value| add_nested(key, value) }
48
+ end
49
+
50
+ def add_key(key, length = nil)
51
+ self[key] ||= self.class.new(model, key, length)
52
+ end
53
+
54
+ def add_nested(key, value, length = nil)
55
+ add_key(key, length).add_spec(value)
56
+ end
57
+
58
+ def process_string(str)
59
+ head, rest = str.split('.', 2)
60
+ k, length = head.split('*', -2)
61
+ add_nested(k.to_sym, rest, length)
62
+ end
63
+
64
+ private
65
+
66
+ def target_class(model, key)
67
+ association = model.associations[key]
68
+ fail "Invalid association: #{[*path, key].join('.')}" unless association
69
+ model.associations[key].target_class
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,110 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Query
4
+ # Methods related to returning nodes and rels from QueryProxy
5
+ module QueryProxyEnumerable
6
+ include Enumerable
7
+
8
+ # Just like every other <tt>each</tt> but it allows for optional params to support the versions that also return relationships.
9
+ # The <tt>node</tt> and <tt>rel</tt> params are typically used by those other methods but there's nothing stopping you from
10
+ # using `your_node.each(true, true)` instead of `your_node.each_with_rel`.
11
+ # @return [Enumerable] An enumerable containing some combination of nodes and rels.
12
+ def each(node = true, rel = nil, &block)
13
+ result(node, rel).each(&block)
14
+ end
15
+
16
+ def result(node = true, rel = nil)
17
+ return [].freeze if unpersisted_start_object?
18
+
19
+ @result_cache ||= {}
20
+ return result_cache_for(node, rel) if result_cache?(node, rel)
21
+
22
+ result = pluck_vars(node, rel)
23
+ set_instance_caches(result, node, rel)
24
+
25
+ @result_cache[[node, rel]] ||= result
26
+ end
27
+
28
+ def result_cache?(node = true, rel = nil)
29
+ !!result_cache_for(node, rel)
30
+ end
31
+
32
+ def result_cache_for(node = true, rel = nil)
33
+ (@result_cache || {})[[node, rel]]
34
+ end
35
+
36
+ def fetch_result_cache
37
+ @result_cache ||= yield
38
+ end
39
+
40
+ # When called at the end of a QueryProxy chain, it will return the resultant relationship objects intead of nodes.
41
+ # For example, to return the relationship between a given student and their lessons:
42
+ #
43
+ # .. code-block:: ruby
44
+ #
45
+ # student.lessons.each_rel do |rel|
46
+ #
47
+ # @return [Enumerable] An enumerable containing any number of applicable relationship objects.
48
+ def each_rel(&block)
49
+ block_given? ? each(false, true, &block) : to_enum(:each, false, true)
50
+ end
51
+
52
+ # When called at the end of a QueryProxy chain, it will return the nodes and relationships of the last link.
53
+ # For example, to return a lesson and each relationship to a given student:
54
+ #
55
+ # .. code-block:: ruby
56
+ #
57
+ # student.lessons.each_with_rel do |lesson, rel|
58
+ def each_with_rel(&block)
59
+ block_given? ? each(true, true, &block) : to_enum(:each, true, true)
60
+ end
61
+
62
+ # Does exactly what you would hope. Without it, comparing `bobby.lessons == sandy.lessons` would evaluate to false because it
63
+ # would be comparing the QueryProxy objects, not the lessons themselves.
64
+ def ==(other)
65
+ self.to_a == other
66
+ end
67
+
68
+ # For getting variables which have been defined as part of the association chain
69
+ def pluck(*args)
70
+ transformable_attributes = (model ? model.attribute_names : []) + %w(uuid neo_id)
71
+ arg_list = args.map do |arg|
72
+ arg = ActiveGraph::Node::Query::QueryProxy::Link.converted_key(model, arg)
73
+ if transformable_attributes.include?(arg.to_s)
74
+ {identity => arg}
75
+ else
76
+ arg
77
+ end
78
+ end
79
+
80
+ self.query.pluck(*arg_list)
81
+ end
82
+
83
+ protected
84
+
85
+ def ensure_distinct(node, force = false)
86
+ @distinct || force ? "DISTINCT(#{node})" : node
87
+ end
88
+
89
+ private
90
+
91
+ def pluck_vars(node, rel)
92
+ vars = []
93
+ vars << ensure_distinct(identity) if node
94
+ vars << @rel_var if rel
95
+ pluck(*vars)
96
+ end
97
+
98
+ def set_instance_caches(instance, node, rel)
99
+ instance.each do |object|
100
+ object.instance_variable_set('@source_query_proxy', self)
101
+ object.instance_variable_set('@source_proxy_result_cache', instance)
102
+ if node && rel && object.last.is_a?(ActiveGraph::Relationship)
103
+ object.last.instance_variable_set(association.direction == :in ? '@from_node' : '@to_node', object.first)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Query
4
+ module QueryProxyFindInBatches
5
+ def find_in_batches(options = {})
6
+ query.return(identity).find_in_batches(identity, @model.primary_key, options) do |batch|
7
+ yield batch.map { |record| record[identity] }
8
+ end
9
+ end
10
+
11
+ def find_each(options = {})
12
+ query.return(identity).find_each(identity, @model.primary_key, options) do |result|
13
+ yield result[identity]
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,139 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Query
4
+ class QueryProxy
5
+ class Link
6
+ attr_reader :clause
7
+
8
+ def initialize(clause, arg, args = [])
9
+ @clause = clause
10
+ @arg = arg
11
+ @args = args
12
+ end
13
+
14
+ def args(var, rel_var)
15
+ if @arg.respond_to?(:call)
16
+ @arg.call(var, rel_var)
17
+ else
18
+ [@arg] + @args
19
+ end
20
+ end
21
+
22
+ class << self
23
+ def for_clause(clause, arg, model, *args)
24
+ method_to_call = "for_#{clause}_clause"
25
+
26
+ send(method_to_call, arg, model, *args)
27
+ end
28
+
29
+ def for_where_clause(arg, model, *args)
30
+ node_num = 1
31
+ result = []
32
+ if arg.is_a?(Hash)
33
+ arg.each do |key, value|
34
+ if model && model.association?(key)
35
+ result += for_association(key, value, "n#{node_num}", model)
36
+ node_num += 1
37
+ else
38
+ result << new_for_key_and_value(model, key, value)
39
+ end
40
+ end
41
+ elsif arg.is_a?(String)
42
+ result << new(:where, arg, args)
43
+ end
44
+ result
45
+ end
46
+ alias for_node_where_clause for_where_clause
47
+
48
+ def for_where_not_clause(*args)
49
+ for_where_clause(*args).each do |link|
50
+ link.instance_variable_set('@clause', :where_not)
51
+ end
52
+ end
53
+
54
+ def new_for_key_and_value(model, key, value)
55
+ key = converted_key(model, key)
56
+
57
+ val = if !model
58
+ value
59
+ elsif key == model.id_property_name && value.is_a?(ActiveGraph::Node)
60
+ value.id
61
+ else
62
+ converted_value(model, key, value)
63
+ end
64
+
65
+ new(:where, ->(v, _) { {v => {key => val}} })
66
+ end
67
+
68
+ def for_association(name, value, n_string, model)
69
+ neo_id = value.try(:neo_id) || value
70
+ fail ArgumentError, "Invalid value for '#{name}' condition" if not neo_id.is_a?(Integer)
71
+
72
+ [
73
+ new(:match, ->(v, _) { "(#{v})#{model.associations[name].arrow_cypher}(#{n_string})" }),
74
+ new(:where, ->(_, _) { {"ID(#{n_string})" => neo_id.to_i} })
75
+ ]
76
+ end
77
+
78
+ # We don't accept strings here. If you want to use a string, just use where.
79
+ def for_rel_where_clause(arg, _, association)
80
+ arg.each_with_object([]) do |(key, value), result|
81
+ rel_class = association.relationship_class if association.relationship_class
82
+ val = rel_class ? converted_value(rel_class, key, value) : value
83
+ result << new(:where, ->(_, rel_var) { {rel_var => {key => val}} })
84
+ end
85
+ end
86
+
87
+ def for_rel_where_not_clause(*args)
88
+ for_rel_where_clause(*args).each do |link|
89
+ link.instance_variable_set('@clause', :where_not)
90
+ end
91
+ end
92
+
93
+ def for_rel_order_clause(arg, _)
94
+ [new(:order, ->(_, v) { arg.is_a?(String) ? arg : {v => arg} })]
95
+ end
96
+
97
+ def for_order_clause(arg, model)
98
+ [new(:order, ->(v, _) { arg.is_a?(String) ? arg : {v => converted_keys(model, arg)} })]
99
+ end
100
+
101
+ def for_args(model, clause, args, association = nil)
102
+ if [:where, :where_not].include?(clause) && args[0].is_a?(String) # Better way?
103
+ [for_arg(model, clause, args[0], *args[1..-1])]
104
+ elsif [:rel_where, :rel_where_not].include?(clause)
105
+ args.map { |arg| for_arg(model, clause, arg, association) }
106
+ else
107
+ args.map { |arg| for_arg(model, clause, arg) }
108
+ end
109
+ end
110
+
111
+ def for_arg(model, clause, arg, *args)
112
+ default = [Link.new(clause, arg, *args)]
113
+
114
+ Link.for_clause(clause, arg, model, *args) || default
115
+ rescue NoMethodError
116
+ default
117
+ end
118
+
119
+ def converted_keys(model, arg)
120
+ arg.is_a?(Hash) ? Hash[arg.map { |key, value| [converted_key(model, key), value] }] : arg
121
+ end
122
+
123
+ def converted_key(model, key)
124
+ if key.to_sym == :id
125
+ model ? model.id_property_name : :uuid
126
+ else
127
+ key
128
+ end
129
+ end
130
+
131
+ def converted_value(model, key, value)
132
+ model.declared_properties.value_for_where(key, value)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,303 @@
1
+ module ActiveGraph
2
+ module Node
3
+ module Query
4
+ # rubocop:disable Metrics/ModuleLength
5
+ module QueryProxyMethods
6
+ # rubocop:enable Metrics/ModuleLength
7
+ FIRST = 'HEAD'
8
+ LAST = 'LAST'
9
+
10
+ def rels
11
+ fail 'Cannot get rels without a relationship variable.' if !@rel_var
12
+
13
+ pluck(@rel_var)
14
+ end
15
+
16
+ def rel
17
+ rels.first
18
+ end
19
+
20
+ def as(node_var)
21
+ new_link(node_var)
22
+ end
23
+
24
+ # Give ability to call `#find` on associations to get a scoped find
25
+ # Doesn't pass through via `method_missing` because Enumerable has a `#find` method
26
+ def find(*args)
27
+ scoping { @model.find(*args) }
28
+ end
29
+
30
+ def first(target = nil)
31
+ first_and_last(FIRST, target)
32
+ end
33
+
34
+ def last(target = nil)
35
+ first_and_last(LAST, target)
36
+ end
37
+
38
+ def order_property
39
+ # This should maybe be based on a setting in the association
40
+ # rather than a hardcoded `nil`
41
+ model ? model.id_property_name : nil
42
+ end
43
+
44
+ def distinct
45
+ new_link.tap do |e|
46
+ e.instance_variable_set(:@distinct, true)
47
+ end
48
+ end
49
+
50
+ def propagate_context(query_proxy)
51
+ query_proxy.instance_variable_set(:@distinct, @distinct)
52
+ end
53
+
54
+ # @return [Integer] number of nodes of this class
55
+ def count(distinct = nil, target = nil)
56
+ return 0 if unpersisted_start_object?
57
+ fail(ActiveGraph::InvalidParameterError, ':count accepts the `:distinct` symbol or nil as a parameter') unless distinct.nil? || distinct == :distinct
58
+ query_with_target(target) do |var|
59
+ q = ensure_distinct(var, !distinct.nil?)
60
+ limited_query = self.query.clause?(:limit) ? self.query.break.with(var) : self.query.reorder
61
+ limited_query.pluck("count(#{q}) AS #{var}").first
62
+ end
63
+ end
64
+
65
+ def size
66
+ result_cache? ? result_cache_for.length : count
67
+ end
68
+
69
+ delegate :length, to: :to_a
70
+
71
+ # TODO: update this with public API methods if/when they are exposed
72
+ def limit_value
73
+ return unless self.query.clause?(:limit)
74
+ limit_clause = self.query.send(:clauses).find { |clause| clause.is_a?(ActiveGraph::Core::QueryClauses::LimitClause) }
75
+ limit_clause.instance_variable_get(:@arg)
76
+ end
77
+
78
+ def empty?(target = nil)
79
+ return true if unpersisted_start_object?
80
+ query_with_target(target) { |var| !self.exists?(nil, var) }
81
+ end
82
+
83
+ alias blank? empty?
84
+
85
+ # @param [ActiveGraph::Node, ActiveGraph::Node, String] other An instance of a Neo4j.rb model, a core node, or a string uuid
86
+ # @param [String, Symbol] target An identifier of a link in the Cypher chain
87
+ # @return [Boolean]
88
+ def include?(other, target = nil)
89
+ query_with_target(target) do |var|
90
+ where_filter = if other.respond_to?(:neo_id) || association_id_key == :neo_id
91
+ "ID(#{var}) = $other_node_id"
92
+ else
93
+ "#{var}.#{association_id_key} = $other_node_id"
94
+ end
95
+ node_id = other.respond_to?(:neo_id) ? other.neo_id : other
96
+ self.where(where_filter).params(other_node_id: node_id).query.reorder.return("count(#{var}) as count")
97
+ .first[:count].positive?
98
+ end
99
+ end
100
+
101
+ def exists?(node_condition = nil, target = nil)
102
+ unless [Integer, String, Hash, NilClass].any? { |c| node_condition.is_a?(c) }
103
+ fail(ActiveGraph::InvalidParameterError, ':exists? only accepts ids or conditions')
104
+ end
105
+ query_with_target(target) do |var|
106
+ start_q = exists_query_start(node_condition, var)
107
+ result = start_q.query.reorder.return("ID(#{var}) AS proof_of_life LIMIT 1").first
108
+ !!result
109
+ end
110
+ end
111
+
112
+ # Shorthand for `MATCH (start)-[r]-(other_node) WHERE ID(other_node) = #{other_node.neo_id}`
113
+ # The `node` param can be a persisted Node instance, any string or integer, or nil.
114
+ # When it's a node, it'll use the object's neo_id, which is fastest. When not nil, it'll figure out the
115
+ # primary key of that model. When nil, it uses `1 = 2` to prevent matching all records, which is the default
116
+ # behavior when nil is passed to `where` in QueryProxy.
117
+ # @param [#neo_id, String, Enumerable] node A node, a string representing a node's ID, or an enumerable of nodes or IDs.
118
+ # @return [ActiveGraph::Node::Query::QueryProxy] A QueryProxy object upon which you can build.
119
+ def match_to(node)
120
+ first_node = node.is_a?(Array) ? node.first : node
121
+ where_arg = if first_node.respond_to?(:neo_id)
122
+ {neo_id: node.is_a?(Array) ? node.map(&:neo_id) : node}
123
+ elsif !node.nil?
124
+ {association_id_key => node.is_a?(Array) ? ids_array(node) : node}
125
+ else
126
+ # support for null object pattern
127
+ '1 = 2'
128
+ end
129
+
130
+ self.where(where_arg)
131
+ end
132
+
133
+
134
+ # Gives you the first relationship between the last link of a QueryProxy chain and a given node
135
+ # Shorthand for `MATCH (start)-[r]-(other_node) WHERE ID(other_node) = #{other_node.neo_id} RETURN r`
136
+ # @param [#neo_id, String, Enumerable] node An object to be sent to `match_to`. See params for that method.
137
+ # @return A relationship (Relationship, CypherRelationship, EmbeddedRelationship) or nil.
138
+ def first_rel_to(node)
139
+ self.match_to(node).limit(1).pluck(rel_var).first
140
+ end
141
+
142
+ # Returns all relationships across a QueryProxy chain between a given node or array of nodes and the preceeding link.
143
+ # @param [#neo_id, String, Enumerable] node An object to be sent to `match_to`. See params for that method.
144
+ # @return An enumerable of relationship objects.
145
+ def rels_to(node)
146
+ self.match_to(node).pluck(rel_var)
147
+ end
148
+ alias all_rels_to rels_to
149
+
150
+ # When called, this method returns a single node that satisfies the match specified in the params hash.
151
+ # If no existing node is found to satisfy the match, one is created or associated as expected.
152
+ def find_or_create_by(params)
153
+ fail 'Method invalid when called on Class objects' unless source_object
154
+ result = self.where(params).first
155
+ return result unless result.nil?
156
+ ActiveGraph::Base.transaction do
157
+ node = model.create(params)
158
+ self << node
159
+ node
160
+ end
161
+ end
162
+
163
+ def find_or_initialize_by(attributes, &block)
164
+ find_by(attributes) || initialize_by_current_chain_params(attributes, &block)
165
+ end
166
+
167
+ def first_or_initialize(attributes = {}, &block)
168
+ first || initialize_by_current_chain_params(attributes, &block)
169
+ end
170
+
171
+ # A shortcut for attaching a new, optional match to the end of a QueryProxy chain.
172
+ def optional(association, node_var = nil, rel_var = nil)
173
+ self.send(association, node_var, rel_var, optional: true)
174
+ end
175
+
176
+ # Takes an Array of Node models and applies the appropriate WHERE clause
177
+ # So for a `Teacher` model inheriting from a `Person` model and an `Article` model
178
+ # if you called .as_models([Teacher, Article])
179
+ # The where clause would look something like:
180
+ #
181
+ # .. code-block:: cypher
182
+ #
183
+ # WHERE (node_var:Teacher:Person OR node_var:Article)
184
+ def as_models(models)
185
+ where_clause = models.map do |model|
186
+ "`#{identity}`:" + model.mapped_label_names.map do |mapped_label_name|
187
+ "`#{mapped_label_name}`"
188
+ end.join(':')
189
+ end.join(' OR ')
190
+
191
+ where("(#{where_clause})")
192
+ end
193
+
194
+ # Matches all nodes having at least a relation
195
+ #
196
+ # @example Load all people having a friend
197
+ # Person.all.having_rel(:friends).to_a # => Returns a list of `Person`
198
+ #
199
+ # @example Load all people having a best friend
200
+ # Person.all.having_rel(:friends, best: true).to_a # => Returns a list of `Person`
201
+ #
202
+ # @return [QueryProxy] A new QueryProxy
203
+ def having_rel(association_name, rel_properties = {})
204
+ association = association_or_fail(association_name)
205
+ where("(#{identity})#{association.arrow_cypher(nil, rel_properties)}()")
206
+ end
207
+
208
+ # Matches all nodes not having a certain relation
209
+ #
210
+ # @example Load all people not having friends
211
+ # Person.all.not_having_rel(:friends).to_a # => Returns a list of `Person`
212
+ #
213
+ # @example Load all people not having best friends
214
+ # Person.all.not_having_rel(:friends, best: true).to_a # => Returns a list of `Person`
215
+ #
216
+ # @return [QueryProxy] A new QueryProxy
217
+ def not_having_rel(association_name, rel_properties = {})
218
+ association = association_or_fail(association_name)
219
+ where_not("(#{identity})#{association.arrow_cypher(nil, rel_properties)}()")
220
+ end
221
+
222
+ private
223
+
224
+ def association_or_fail(association_name)
225
+ model.associations[association_name] || fail(ArgumentError, "No such association #{association_name}")
226
+ end
227
+
228
+ def find_inverse_association!(model, source, association)
229
+ model.associations.values.find do |reverse_association|
230
+ association.inverse_of?(reverse_association) ||
231
+ reverse_association.inverse_of?(association) ||
232
+ inverse_relation_of?(source, association, model, reverse_association)
233
+ end || fail("Could not find reverse association for #{@context}")
234
+ end
235
+
236
+ def inverse_relation_of?(source, source_association, target, target_association)
237
+ source_association.direction != target_association.direction &&
238
+ source == target_association.target_class &&
239
+ target == source_association.target_class &&
240
+ source_association.relationship_class_name == target_association.relationship_class_name
241
+ end
242
+
243
+ def initialize_by_current_chain_params(params = {})
244
+ result = new(where_clause_params.merge(params))
245
+
246
+ inverse_association = find_inverse_association!(model, source_object.class, association) if source_object
247
+ result.tap do |m|
248
+ yield(m) if block_given?
249
+ m.public_send(inverse_association.name) << source_object if inverse_association
250
+ end
251
+ end
252
+
253
+ def where_clause_params
254
+ query.clauses.select { |c| c.is_a?(ActiveGraph::Core::QueryClauses::WhereClause) && c.arg.is_a?(Hash) }
255
+ .map! { |e| e.arg[identity] }.compact.inject { |a, b| a.merge(b) } || {}
256
+ end
257
+
258
+ def first_and_last(func, target)
259
+ new_query, pluck_proc = if self.query.clause?(:order)
260
+ [self.query.with(identity),
261
+ proc { |var| "#{func}(COLLECT(#{var})) as #{var}" }]
262
+ else
263
+ ord_prop = (func == LAST ? {order_property => :DESC} : order_property)
264
+ [self.order(ord_prop).limit(1),
265
+ proc { |var| var }]
266
+ end
267
+ query_with_target(target) do |var|
268
+ final_pluck = pluck_proc.call(var)
269
+ new_query.pluck(final_pluck)
270
+ end.first
271
+ end
272
+
273
+ # @return [String] The primary key of a the current QueryProxy's model or target class
274
+ def association_id_key
275
+ self.association.nil? ? model.primary_key : self.association.target_class.primary_key
276
+ end
277
+
278
+ # @param [Enumerable] node An enumerable of nodes or ids.
279
+ # @return [Array] An array after having `id` called on each object
280
+ def ids_array(node)
281
+ node.first.respond_to?(:id) ? node.map(&:id) : node
282
+ end
283
+
284
+ def query_with_target(target)
285
+ yield(target || identity)
286
+ end
287
+
288
+ def exists_query_start(condition, target)
289
+ case condition
290
+ when Integer
291
+ self.where("ID(#{target}) = $exists_condition").params(exists_condition: condition)
292
+ when Hash
293
+ self.where(condition.keys.first => condition.values.first)
294
+ when String
295
+ self.where(model.primary_key => condition)
296
+ else
297
+ self
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
303
+ end