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.
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