neo4j_legacy 7.2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1357 -0
  3. data/CONTRIBUTORS +8 -0
  4. data/Gemfile +38 -0
  5. data/README.md +103 -0
  6. data/bin/neo4j-jars +33 -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_support/per_thread_registry.rb +1 -0
  12. data/lib/backports/action_controller/metal/strong_parameters.rb +672 -0
  13. data/lib/backports/active_model/forbidden_attributes_protection.rb +30 -0
  14. data/lib/backports/active_support/concern.rb +13 -0
  15. data/lib/backports/active_support/core_ext/module/attribute_accessors.rb +10 -0
  16. data/lib/backports/active_support/logger.rb +99 -0
  17. data/lib/backports/active_support/logger_silence.rb +27 -0
  18. data/lib/backports/active_support/logger_thread_safe_level.rb +32 -0
  19. data/lib/backports/active_support/per_thread_registry.rb +60 -0
  20. data/lib/backports.rb +4 -0
  21. data/lib/neo4j/active_node/callbacks.rb +8 -0
  22. data/lib/neo4j/active_node/dependent/association_methods.rb +48 -0
  23. data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +50 -0
  24. data/lib/neo4j/active_node/dependent.rb +11 -0
  25. data/lib/neo4j/active_node/enum.rb +29 -0
  26. data/lib/neo4j/active_node/has_n/association/rel_factory.rb +61 -0
  27. data/lib/neo4j/active_node/has_n/association/rel_wrapper.rb +23 -0
  28. data/lib/neo4j/active_node/has_n/association.rb +280 -0
  29. data/lib/neo4j/active_node/has_n/association_cypher_methods.rb +108 -0
  30. data/lib/neo4j/active_node/has_n.rb +532 -0
  31. data/lib/neo4j/active_node/id_property/accessor.rb +62 -0
  32. data/lib/neo4j/active_node/id_property.rb +187 -0
  33. data/lib/neo4j/active_node/initialize.rb +21 -0
  34. data/lib/neo4j/active_node/labels/index.rb +87 -0
  35. data/lib/neo4j/active_node/labels/reloading.rb +21 -0
  36. data/lib/neo4j/active_node/labels.rb +198 -0
  37. data/lib/neo4j/active_node/node_wrapper.rb +52 -0
  38. data/lib/neo4j/active_node/orm_adapter.rb +82 -0
  39. data/lib/neo4j/active_node/persistence.rb +175 -0
  40. data/lib/neo4j/active_node/property.rb +60 -0
  41. data/lib/neo4j/active_node/query/query_proxy.rb +361 -0
  42. data/lib/neo4j/active_node/query/query_proxy_eager_loading.rb +61 -0
  43. data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +90 -0
  44. data/lib/neo4j/active_node/query/query_proxy_find_in_batches.rb +19 -0
  45. data/lib/neo4j/active_node/query/query_proxy_link.rb +117 -0
  46. data/lib/neo4j/active_node/query/query_proxy_methods.rb +210 -0
  47. data/lib/neo4j/active_node/query/query_proxy_methods_of_mass_updating.rb +83 -0
  48. data/lib/neo4j/active_node/query.rb +76 -0
  49. data/lib/neo4j/active_node/query_methods.rb +65 -0
  50. data/lib/neo4j/active_node/reflection.rb +86 -0
  51. data/lib/neo4j/active_node/rels.rb +11 -0
  52. data/lib/neo4j/active_node/scope.rb +146 -0
  53. data/lib/neo4j/active_node/unpersisted.rb +48 -0
  54. data/lib/neo4j/active_node/validations.rb +59 -0
  55. data/lib/neo4j/active_node.rb +105 -0
  56. data/lib/neo4j/active_rel/callbacks.rb +15 -0
  57. data/lib/neo4j/active_rel/initialize.rb +28 -0
  58. data/lib/neo4j/active_rel/persistence/query_factory.rb +95 -0
  59. data/lib/neo4j/active_rel/persistence.rb +114 -0
  60. data/lib/neo4j/active_rel/property.rb +95 -0
  61. data/lib/neo4j/active_rel/query.rb +95 -0
  62. data/lib/neo4j/active_rel/rel_wrapper.rb +22 -0
  63. data/lib/neo4j/active_rel/related_node.rb +83 -0
  64. data/lib/neo4j/active_rel/types.rb +82 -0
  65. data/lib/neo4j/active_rel/validations.rb +8 -0
  66. data/lib/neo4j/active_rel.rb +67 -0
  67. data/lib/neo4j/class_arguments.rb +39 -0
  68. data/lib/neo4j/config.rb +124 -0
  69. data/lib/neo4j/core/query.rb +22 -0
  70. data/lib/neo4j/errors.rb +28 -0
  71. data/lib/neo4j/migration.rb +127 -0
  72. data/lib/neo4j/paginated.rb +27 -0
  73. data/lib/neo4j/railtie.rb +169 -0
  74. data/lib/neo4j/schema/operation.rb +91 -0
  75. data/lib/neo4j/shared/attributes.rb +220 -0
  76. data/lib/neo4j/shared/callbacks.rb +64 -0
  77. data/lib/neo4j/shared/cypher.rb +37 -0
  78. data/lib/neo4j/shared/declared_properties.rb +204 -0
  79. data/lib/neo4j/shared/declared_property/index.rb +37 -0
  80. data/lib/neo4j/shared/declared_property.rb +118 -0
  81. data/lib/neo4j/shared/enum.rb +148 -0
  82. data/lib/neo4j/shared/filtered_hash.rb +79 -0
  83. data/lib/neo4j/shared/identity.rb +28 -0
  84. data/lib/neo4j/shared/initialize.rb +28 -0
  85. data/lib/neo4j/shared/marshal.rb +23 -0
  86. data/lib/neo4j/shared/mass_assignment.rb +58 -0
  87. data/lib/neo4j/shared/permitted_attributes.rb +28 -0
  88. data/lib/neo4j/shared/persistence.rb +231 -0
  89. data/lib/neo4j/shared/property.rb +220 -0
  90. data/lib/neo4j/shared/query_factory.rb +101 -0
  91. data/lib/neo4j/shared/rel_type_converters.rb +43 -0
  92. data/lib/neo4j/shared/serialized_properties.rb +30 -0
  93. data/lib/neo4j/shared/type_converters.rb +418 -0
  94. data/lib/neo4j/shared/typecasted_attributes.rb +98 -0
  95. data/lib/neo4j/shared/typecaster.rb +53 -0
  96. data/lib/neo4j/shared/validations.rb +48 -0
  97. data/lib/neo4j/shared.rb +51 -0
  98. data/lib/neo4j/tasks/migration.rake +24 -0
  99. data/lib/neo4j/timestamps/created.rb +9 -0
  100. data/lib/neo4j/timestamps/updated.rb +9 -0
  101. data/lib/neo4j/timestamps.rb +11 -0
  102. data/lib/neo4j/type_converters.rb +7 -0
  103. data/lib/neo4j/version.rb +3 -0
  104. data/lib/neo4j/wrapper.rb +4 -0
  105. data/lib/neo4j.rb +96 -0
  106. data/lib/rails/generators/neo4j/model/model_generator.rb +86 -0
  107. data/lib/rails/generators/neo4j/model/templates/model.erb +15 -0
  108. data/lib/rails/generators/neo4j_generator.rb +67 -0
  109. data/neo4j.gemspec +43 -0
  110. metadata +389 -0
@@ -0,0 +1,175 @@
1
+ module Neo4j::ActiveNode
2
+ module Persistence
3
+ class RecordInvalidError < RuntimeError
4
+ attr_reader :record
5
+
6
+ def initialize(record)
7
+ @record = record
8
+ super(@record.errors.full_messages.join(', '))
9
+ end
10
+ end
11
+
12
+ extend ActiveSupport::Concern
13
+ extend Forwardable
14
+ include Neo4j::Shared::Persistence
15
+
16
+ # Saves the model.
17
+ #
18
+ # If the model is new a record gets created in the database, otherwise the existing record gets updated.
19
+ # If perform_validation is true validations run.
20
+ # If any of them fail the action is cancelled and save returns false.
21
+ # If the flag is false validations are bypassed altogether.
22
+ # See ActiveRecord::Validations for more information.
23
+ # There's a series of callbacks associated with save.
24
+ # If any of the before_* callbacks return false the action is cancelled and save returns false.
25
+ def save(*)
26
+ cascade_save do
27
+ association_proxy_cache.clear
28
+ create_or_update
29
+ end
30
+ end
31
+
32
+ # Increments concurrently a numeric attribute by a centain amount
33
+ # @param [Symbol, String] name of the attribute to increment
34
+ # @param [Integer, Float] amount to increment
35
+ def concurrent_increment!(attribute, by = 1)
36
+ query_node = Neo4j::Session.query.match_nodes(n: neo_id)
37
+ increment_by_query! query_node, attribute, by
38
+ end
39
+
40
+ # Persist the object to the database. Validations and Callbacks are included
41
+ # by default but validation can be disabled by passing :validate => false
42
+ # to #save! Creates a new transaction.
43
+ #
44
+ # @raise a RecordInvalidError if there is a problem during save.
45
+ # @param (see Neo4j::Rails::Validations#save)
46
+ # @return nil
47
+ # @see #save
48
+ # @see Neo4j::Rails::Validations Neo4j::Rails::Validations - for the :validate parameter
49
+ # @see Neo4j::Rails::Callbacks Neo4j::Rails::Callbacks - for callbacks
50
+ def save!(*args)
51
+ save(*args) or fail(RecordInvalidError, self) # rubocop:disable Style/AndOr
52
+ end
53
+
54
+ # Creates a model with values matching those of the instance attributes and returns its id.
55
+ # @private
56
+ # @return true
57
+ def create_model
58
+ node = _create_node(props_for_create)
59
+ init_on_load(node, node.props)
60
+ @deferred_nodes = nil
61
+ true
62
+ end
63
+
64
+ # TODO: This does not seem like it should be the responsibility of the node.
65
+ # Creates an unwrapped node in the database.
66
+ # @param [Hash] node_props The type-converted properties to be added to the new node.
67
+ # @param [Array] labels The labels to use for creating the new node.
68
+ # @return [Neo4j::Node] A CypherNode or EmbeddedNode
69
+ def _create_node(node_props, labels = labels_for_create)
70
+ self.class.neo4j_session.create_node(node_props, labels)
71
+ end
72
+
73
+ # As the name suggests, this inserts the primary key (id property) into the properties hash.
74
+ # The method called here, `default_property_values`, is a holdover from an earlier version of the gem. It does NOT
75
+ # contain the default values of properties, it contains the Default Property, which we now refer to as the ID Property.
76
+ # It will be deprecated and renamed in a coming refactor.
77
+ # @param [Hash] converted_props A hash of properties post-typeconversion, ready for insertion into the DB.
78
+ def inject_primary_key!(converted_props)
79
+ self.class.default_property_values(self).tap do |destination_props|
80
+ destination_props.merge!(converted_props) if converted_props.is_a?(Hash)
81
+ end
82
+ end
83
+
84
+ # @return [Array] Labels to be set on the node during a create event
85
+ def labels_for_create
86
+ self.class.mapped_label_names
87
+ end
88
+
89
+ private
90
+
91
+ # The pending associations are cleared during the save process, so it's necessary to
92
+ # build the processable hash before it begins. If there are nodes and associations that
93
+ # need to be created after the node is saved, a new transaction is started.
94
+ def cascade_save
95
+ Neo4j::Transaction.run(pending_deferred_creations?) do
96
+ result = yield
97
+ process_unpersisted_nodes!
98
+ result
99
+ end
100
+ end
101
+
102
+ module ClassMethods
103
+ # Creates and saves a new node
104
+ # @param [Hash] props the properties the new node should have
105
+ def create(props = {})
106
+ new(props).tap do |obj|
107
+ yield obj if block_given?
108
+ obj.save
109
+ end
110
+ end
111
+
112
+ # Same as #create, but raises an error if there is a problem during save.
113
+ def create!(props = {})
114
+ new(props).tap do |o|
115
+ yield o if block_given?
116
+ o.save!
117
+ end
118
+ end
119
+
120
+ def merge(match_attributes, optional_attrs = {})
121
+ options = [:on_create, :on_match, :set]
122
+ optional_attrs.assert_valid_keys(*options)
123
+
124
+ optional_attrs.default = {}
125
+ on_create_attrs, on_match_attrs, set_attrs = optional_attrs.values_at(*options)
126
+
127
+ neo4j_session.query.merge(n: {self.mapped_label_names => match_attributes})
128
+ .on_create_set(on_create_clause(on_create_attrs))
129
+ .on_match_set(on_match_clause(on_match_attrs))
130
+ .break.set(n: set_attrs)
131
+ .pluck(:n).first
132
+ end
133
+
134
+ def find_or_create(find_attributes, set_attributes = {})
135
+ on_create_attributes = set_attributes.reverse_merge(find_attributes.merge(self.new(find_attributes).props_for_create))
136
+
137
+ neo4j_session.query.merge(n: {self.mapped_label_names => find_attributes})
138
+ .on_create_set(n: on_create_attributes)
139
+ .pluck(:n).first
140
+ end
141
+
142
+ # Finds the first node with the given attributes, or calls create if none found
143
+ def find_or_create_by(attributes, &block)
144
+ find_by(attributes) || create(attributes, &block)
145
+ end
146
+
147
+ # Same as #find_or_create_by, but calls #create! so it raises an error if there is a problem during save.
148
+ def find_or_create_by!(attributes, &block)
149
+ find_by(attributes) || create!(attributes, &block)
150
+ end
151
+
152
+ def load_entity(id)
153
+ Neo4j::Node.load(id)
154
+ end
155
+
156
+ private
157
+
158
+ def on_create_clause(clause)
159
+ if clause.is_a?(Hash)
160
+ {n: clause.merge(self.new(clause).props_for_create)}
161
+ else
162
+ clause
163
+ end
164
+ end
165
+
166
+ def on_match_clause(clause)
167
+ if clause.is_a?(Hash)
168
+ {n: clause.merge(attributes_nil_hash.key?('updated_at') ? {updated_at: Time.new.to_i} : {})}
169
+ else
170
+ clause
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,60 @@
1
+ module Neo4j::ActiveNode
2
+ module Property
3
+ extend ActiveSupport::Concern
4
+ include Neo4j::Shared::Property
5
+
6
+ def initialize(attributes = nil)
7
+ super(attributes)
8
+ @attributes ||= Hash[self.class.attributes_nil_hash]
9
+ end
10
+
11
+ module ClassMethods
12
+ # Extracts keys from attributes hash which are associations of the model
13
+ # TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save?
14
+ def extract_association_attributes!(attributes)
15
+ return unless contains_association?(attributes)
16
+ attributes.each_with_object({}) do |(key, _), result|
17
+ result[key] = attributes.delete(key) if self.association_key?(key)
18
+ end
19
+ end
20
+
21
+ def association_key?(key)
22
+ association_method_keys.include?(key.to_sym)
23
+ end
24
+
25
+ private
26
+
27
+ def contains_association?(attributes)
28
+ return false unless attributes
29
+ attributes.each_key { |k| return true if association_key?(k) }
30
+ false
31
+ end
32
+
33
+ # All keys which could be association setter methods (including _id/_ids)
34
+ def association_method_keys
35
+ @association_method_keys ||=
36
+ associations_keys.map(&:to_sym) +
37
+ associations.values.map do |association|
38
+ if association.type == :has_one
39
+ "#{association.name}_id"
40
+ elsif association.type == :has_many
41
+ "#{association.name.to_s.singularize}_ids"
42
+ end.to_sym
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def inspect_attributes
50
+ id_property_name = self.class.id_property_name.to_s
51
+
52
+ attribute_pairs = attributes.except(id_property_name).sort.map do |key, value|
53
+ [key, (value.is_a?(String) && value.size > 100) ? value.dup[0..100] : value]
54
+ end
55
+
56
+ attribute_pairs.unshift([id_property_name, self.send(id_property_name)])
57
+ attribute_pairs
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,361 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Query
4
+ class QueryProxy
5
+ include Neo4j::ActiveNode::Query::QueryProxyEnumerable
6
+ include Neo4j::ActiveNode::Query::QueryProxyMethods
7
+ include Neo4j::ActiveNode::Query::QueryProxyMethodsOfMassUpdating
8
+ include Neo4j::ActiveNode::Query::QueryProxyFindInBatches
9
+ include Neo4j::ActiveNode::Query::QueryProxyEagerLoading
10
+ include Neo4j::ActiveNode::Dependent::QueryProxyMethods
11
+
12
+ # The most recent node to start a QueryProxy chain.
13
+ # Will be nil when using QueryProxy chains on class methods.
14
+ attr_reader :source_object, :association, :model, :starting_query
15
+
16
+ # QueryProxy is ActiveNode's Cypher DSL. While the name might imply that it creates queries in a general sense,
17
+ # it is actually referring to <tt>Neo4j::Core::Query</tt>, which is a pure Ruby Cypher DSL provided by the <tt>neo4j-core</tt> gem.
18
+ # QueryProxy provides ActiveRecord-like methods for common patterns. When it's not handling CRUD for relationships and queries, it
19
+ # provides ActiveNode's association chaining (`student.lessons.teachers.where(age: 30).hobbies`) and enjoys long walks on the
20
+ # beach.
21
+ #
22
+ # It should not ever be necessary to instantiate a new QueryProxy object directly, it always happens as a result of
23
+ # calling a method that makes use of it.
24
+ #
25
+ # @param [Constant] model The class which included ActiveNode (typically a model, hence the name) from which the query
26
+ # originated.
27
+ # @param [Neo4j::ActiveNode::HasN::Association] association The ActiveNode association (an object created by a <tt>has_one</tt> or
28
+ # <tt>has_many</tt>) that created this object.
29
+ # @param [Hash] options Additional options pertaining to the QueryProxy object. These may include:
30
+ # @option options [String, Symbol] :node_var A string or symbol to be used by Cypher within its query string as an identifier
31
+ # @option options [String, Symbol] :rel_var Same as above but pertaining to a relationship identifier
32
+ # @option options [Range, Fixnum, Symbol, Hash] :rel_length A Range, a Fixnum, a Hash or a Symbol to indicate the variable-length/fixed-length
33
+ # qualifier of the relationship. See http://neo4jrb.readthedocs.org/en/latest/Querying.html#variable-length-relationships.
34
+ # @option options [Neo4j::Session] :session The session to be used for this query
35
+ # @option options [Neo4j::ActiveNode] :source_object The node instance at the start of the QueryProxy chain
36
+ # @option options [QueryProxy] :query_proxy An existing QueryProxy chain upon which this new object should be built
37
+ #
38
+ # QueryProxy objects are evaluated lazily.
39
+ def initialize(model, association = nil, options = {})
40
+ @model = model
41
+ @association = association
42
+ @context = options.delete(:context)
43
+ @options = options
44
+ @associations_spec = []
45
+
46
+ instance_vars_from_options!(options)
47
+
48
+ @match_type = @optional ? :optional_match : :match
49
+
50
+ @rel_var = options[:rel] || _rel_chain_var
51
+
52
+ @chain = []
53
+ @params = @query_proxy ? @query_proxy.instance_variable_get('@params') : {}
54
+ end
55
+
56
+ def inspect
57
+ "#<QueryProxy #{@context} CYPHER: #{self.to_cypher.inspect}>"
58
+ end
59
+
60
+ attr_reader :start_object, :query_proxy
61
+
62
+ # The current node identifier on deck, so to speak. It is the object that will be returned by calling `each` and the last node link
63
+ # in the QueryProxy chain.
64
+ attr_reader :node_var
65
+ def identity
66
+ @node_var || _result_string
67
+ end
68
+ alias_method :node_identity, :identity
69
+
70
+ # The relationship identifier most recently used by the QueryProxy chain.
71
+ attr_reader :rel_var
72
+ def rel_identity
73
+ ActiveSupport::Deprecation.warn 'rel_identity is deprecated and may be removed from future releases, use rel_var instead.', caller
74
+
75
+ @rel_var
76
+ end
77
+
78
+ def params(params)
79
+ new_link.tap { |new_query| new_query._add_params(params) }
80
+ end
81
+
82
+ # Like calling #query_as, but for when you don't care about the variable name
83
+ def query
84
+ query_as(identity)
85
+ end
86
+
87
+ # Build a Neo4j::Core::Query object for the QueryProxy. This is necessary when you want to take an existing QueryProxy chain
88
+ # and work with it from the more powerful (but less friendly) Neo4j::Core::Query.
89
+ # @param [String,Symbol] var The identifier to use for node at this link of the QueryProxy chain.
90
+ #
91
+ # .. code-block:: ruby
92
+ #
93
+ # student.lessons.query_as(:l).with('your cypher here...')
94
+ def query_as(var, with_labels = true)
95
+ result_query = @chain.inject(base_query(var, with_labels).params(@params)) do |query, link|
96
+ args = link.args(var, rel_var)
97
+
98
+ args.is_a?(Array) ? query.send(link.clause, *args) : query.send(link.clause, args)
99
+ end
100
+
101
+ result_query.tap { |query| query.proxy_chain_level = _chain_level }
102
+ end
103
+
104
+ def base_query(var, with_labels = true)
105
+ if @association
106
+ chain_var = _association_chain_var
107
+ (_association_query_start(chain_var) & _query).break.send(@match_type,
108
+ "(#{chain_var})#{_association_arrow}(#{var}#{_model_label_string})")
109
+ else
110
+ starting_query ? starting_query : _query_model_as(var, with_labels)
111
+ end
112
+ end
113
+
114
+ # param [TrueClass, FalseClass] with_labels This param is used by certain QueryProxy methods that already have the neo_id and
115
+ # therefore do not need labels.
116
+ # The @association_labels instance var is set during init and used during association chaining to keep labels out of Cypher queries.
117
+ def _model_label_string(with_labels = true)
118
+ return if !@model || (!with_labels || @association_labels == false)
119
+ @model.mapped_label_names.map { |label_name| ":`#{label_name}`" }.join
120
+ end
121
+
122
+ # Scope all queries to the current scope.
123
+ #
124
+ # .. code-block:: ruby
125
+ #
126
+ # Comment.where(post_id: 1).scoping do
127
+ # Comment.first
128
+ # end
129
+ #
130
+ # TODO: unscoped
131
+ # Please check unscoped if you want to remove all previous scopes (including
132
+ # the default_scope) during the execution of a block.
133
+ def scoping
134
+ previous = @model.current_scope
135
+ @model.current_scope = self
136
+ yield
137
+ ensure
138
+ @model.current_scope = previous
139
+ end
140
+
141
+ METHODS = %w(where where_not rel_where rel_order order skip limit)
142
+
143
+ METHODS.each do |method|
144
+ define_method(method) { |*args| build_deeper_query_proxy(method.to_sym, args) }
145
+ end
146
+ # Since there are rel_where and rel_order methods, it seems only natural for there to be node_where and node_order
147
+ alias_method :node_where, :where
148
+ alias_method :node_order, :order
149
+ alias_method :offset, :skip
150
+ alias_method :order_by, :order
151
+
152
+ # Cypher string for the QueryProxy's query. This will not include params. For the full output, see <tt>to_cypher_with_params</tt>.
153
+ delegate :to_cypher, to: :query
154
+
155
+ delegate :print_cypher, to: :query
156
+
157
+ # Returns a string of the cypher query with return objects and params
158
+ # @param [Array] columns array containing symbols of identifiers used in the query
159
+ # @return [String]
160
+ def to_cypher_with_params(columns = [self.identity])
161
+ final_query = query.return_query(columns)
162
+ "#{final_query.to_cypher} | params: #{final_query.send(:merge_params)}"
163
+ end
164
+
165
+ # To add a relationship for the node for the association on this QueryProxy
166
+ def <<(other_node)
167
+ if @start_object._persisted_obj
168
+ create(other_node, {})
169
+ elsif @association
170
+ @start_object.defer_create(@association.name, other_node)
171
+ else
172
+ fail 'Another crazy error!'
173
+ end
174
+ self
175
+ end
176
+
177
+ # Executes the relation chain specified in the block, while keeping the current scope
178
+ #
179
+ # @example Load all people that have friends
180
+ # Person.all.branch { friends }.to_a # => Returns a list of `Person`
181
+ #
182
+ # @example Load all people that has old friends
183
+ # Person.all.branch { friends.where('age > 70') }.to_a # => Returns a list of `Person`
184
+ #
185
+ # @yield the block that will be evaluated starting from the current scope
186
+ #
187
+ # @return [QueryProxy] A new QueryProxy
188
+ def branch(&block)
189
+ if block
190
+ instance_eval(&block).query.proxy_as(self.model, identity)
191
+ else
192
+ fail LocalJumpError, 'no block given'
193
+ end
194
+ end
195
+
196
+ def [](index)
197
+ # TODO: Maybe for this and other methods, use array if already loaded, otherwise
198
+ # use OFFSET and LIMIT 1?
199
+ self.to_a[index]
200
+ end
201
+
202
+ def create(other_nodes, properties = {})
203
+ fail 'Can only create relationships on associations' if !@association
204
+ other_nodes = _nodeify!(*other_nodes)
205
+
206
+ Neo4j::Transaction.run do
207
+ other_nodes.each do |other_node|
208
+ other_node.save unless other_node.neo_id
209
+
210
+ return false if @association.perform_callback(@start_object, other_node, :before) == false
211
+
212
+ @start_object.association_proxy_cache.clear
213
+
214
+ _create_relationship(other_node, properties)
215
+
216
+ @association.perform_callback(@start_object, other_node, :after)
217
+ end
218
+ end
219
+ end
220
+
221
+ def _nodeify!(*args)
222
+ other_nodes = [args].flatten!.map! do |arg|
223
+ (arg.is_a?(Integer) || arg.is_a?(String)) ? @model.find_by(id: arg) : arg
224
+ end.compact
225
+
226
+ if @model && other_nodes.any? { |other_node| !other_node.class.mapped_label_names.include?(@model.mapped_label_name) }
227
+ fail ArgumentError, "Node must be of the association's class when model is specified"
228
+ end
229
+
230
+ other_nodes
231
+ end
232
+
233
+ def _create_relationship(other_node_or_nodes, properties)
234
+ association._create_relationship(@start_object, other_node_or_nodes, properties)
235
+ end
236
+
237
+ def read_attribute_for_serialization(*args)
238
+ to_a.map { |o| o.read_attribute_for_serialization(*args) }
239
+ end
240
+
241
+ delegate :to_ary, to: :to_a
242
+
243
+ # QueryProxy objects act as a representation of a model at the class level so we pass through calls
244
+ # This allows us to define class functions for reusable query chaining or for end-of-query aggregation/summarizing
245
+ def method_missing(method_name, *args, &block)
246
+ if @model && @model.respond_to?(method_name)
247
+ scoping { @model.public_send(method_name, *args, &block) }
248
+ else
249
+ super
250
+ end
251
+ end
252
+
253
+ def respond_to_missing?(method_name, include_all = false)
254
+ (@model && @model.respond_to?(method_name, include_all)) || super
255
+ end
256
+
257
+ def optional?
258
+ @optional == true
259
+ end
260
+
261
+ attr_reader :context
262
+
263
+ def new_link(node_var = nil)
264
+ self.clone.tap do |new_query_proxy|
265
+ new_query_proxy.instance_variable_set('@result_cache', nil)
266
+ new_query_proxy.instance_variable_set('@node_var', node_var) if node_var
267
+ end
268
+ end
269
+
270
+ protected
271
+
272
+ # Methods are underscored to prevent conflict with user class methods
273
+ def _add_params(params)
274
+ @params = @params.merge(params)
275
+ end
276
+
277
+ def _add_links(links)
278
+ @chain += links
279
+ end
280
+
281
+ def _query_model_as(var, with_labels = true)
282
+ _query.break.send(@match_type, _match_arg(var, with_labels))
283
+ end
284
+
285
+ # @param [String, Symbol] var The Cypher identifier to use within the match string
286
+ # @param [Boolean] with_labels Send "true" to include model labels where possible.
287
+ def _match_arg(var, with_labels)
288
+ if @model && with_labels != false
289
+ labels = @model.respond_to?(:mapped_label_names) ? _model_label_string : @model
290
+ {var.to_sym => labels}
291
+ else
292
+ var.to_sym
293
+ end
294
+ end
295
+
296
+ def _query
297
+ _session.query(context: @context)
298
+ end
299
+
300
+ # TODO: Refactor this. Too much happening here.
301
+ def _result_string
302
+ s = (self.association && self.association.name) || (self.model && self.model.name) || ''
303
+
304
+ s ? "result_#{s}".downcase.tr(':', '').to_sym : :result
305
+ end
306
+
307
+ def _session
308
+ (@session || (@model && @model.neo4j_session)).tap do |session|
309
+ fail 'No session found!' if session.nil?
310
+ end
311
+ end
312
+
313
+ def _association_arrow(properties = {}, create = false)
314
+ @association && @association.arrow_cypher(@rel_var, properties, create, false, @rel_length)
315
+ end
316
+
317
+ def _chain_level
318
+ (@query_proxy ? @query_proxy._chain_level : (@chain_level || 0)) + 1
319
+ end
320
+
321
+ def _association_chain_var
322
+ fail 'Crazy error' if !(start_object || @query_proxy)
323
+
324
+ if start_object
325
+ :"#{start_object.class.name.gsub('::', '_').downcase}#{start_object.neo_id}"
326
+ else
327
+ @query_proxy.node_var || :"node#{_chain_level}"
328
+ end
329
+ end
330
+
331
+ def _association_query_start(var)
332
+ # TODO: Better error
333
+ fail 'Crazy error' if !(object = (start_object || @query_proxy))
334
+
335
+ object.query_as(var)
336
+ end
337
+
338
+ def _rel_chain_var
339
+ :"rel#{_chain_level - 1}"
340
+ end
341
+
342
+ attr_writer :context
343
+
344
+ private
345
+
346
+ def instance_vars_from_options!(options)
347
+ @node_var, @session, @source_object, @starting_query, @optional,
348
+ @start_object, @query_proxy, @chain_level, @association_labels,
349
+ @rel_length = options.values_at(:node, :session, :source_object, :starting_query, :optional,
350
+ :start_object, :query_proxy, :chain_level, :association_labels, :rel_length)
351
+ end
352
+
353
+ def build_deeper_query_proxy(method, args)
354
+ new_link.tap do |new_query_proxy|
355
+ Link.for_args(@model, method, args, association).each { |link| new_query_proxy._add_links(link) }
356
+ end
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,61 @@
1
+ module Neo4j
2
+ module ActiveNode
3
+ module Query
4
+ module QueryProxyEagerLoading
5
+ def each(node = true, rel = nil, &block)
6
+ return super if with_associations_spec.size.zero?
7
+
8
+ query_from_association_spec.pluck(identity, "[#{with_associations_return_clause}]").map do |record, eager_data|
9
+ eager_data.each_with_index do |eager_records, index|
10
+ record.association_proxy(with_associations_spec[index]).cache_result(eager_records)
11
+ end
12
+
13
+ block.call(record)
14
+ end
15
+ end
16
+
17
+ def with_associations_spec
18
+ @with_associations_spec ||= []
19
+ end
20
+
21
+ def with_associations(*spec)
22
+ invalid_association_names = spec.reject do |association_name|
23
+ model.associations[association_name]
24
+ end
25
+
26
+ if invalid_association_names.size > 0
27
+ fail "Invalid associations: #{invalid_association_names.join(', ')}"
28
+ end
29
+
30
+ new_link.tap do |new_query_proxy|
31
+ new_spec = new_query_proxy.with_associations_spec + spec
32
+ new_query_proxy.with_associations_spec.replace(new_spec)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def with_associations_return_clause(variables = with_associations_spec)
39
+ variables.map { |n| "#{n}_collection" }.join(',')
40
+ end
41
+
42
+ def query_from_association_spec
43
+ previous_with_variables = []
44
+ with_associations_spec.inject(query_as(identity).with(identity)) do |query, association_name|
45
+ with_association_query_part(query, association_name, previous_with_variables).tap do
46
+ previous_with_variables << association_name
47
+ end
48
+ end.return(identity)
49
+ end
50
+
51
+ def with_association_query_part(base_query, association_name, previous_with_variables)
52
+ association = model.associations[association_name]
53
+
54
+ base_query.optional_match("(#{identity})#{association.arrow_cypher}(#{association_name})")
55
+ .where(association.target_where_clause)
56
+ .with(identity, "collect(#{association_name}) AS #{association_name}_collection", *with_associations_return_clause(previous_with_variables))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end