neo4j_legacy 7.2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1357 -0
- data/CONTRIBUTORS +8 -0
- data/Gemfile +38 -0
- data/README.md +103 -0
- data/bin/neo4j-jars +33 -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_support/per_thread_registry.rb +1 -0
- data/lib/backports/action_controller/metal/strong_parameters.rb +672 -0
- data/lib/backports/active_model/forbidden_attributes_protection.rb +30 -0
- data/lib/backports/active_support/concern.rb +13 -0
- data/lib/backports/active_support/core_ext/module/attribute_accessors.rb +10 -0
- data/lib/backports/active_support/logger.rb +99 -0
- data/lib/backports/active_support/logger_silence.rb +27 -0
- data/lib/backports/active_support/logger_thread_safe_level.rb +32 -0
- data/lib/backports/active_support/per_thread_registry.rb +60 -0
- data/lib/backports.rb +4 -0
- data/lib/neo4j/active_node/callbacks.rb +8 -0
- data/lib/neo4j/active_node/dependent/association_methods.rb +48 -0
- data/lib/neo4j/active_node/dependent/query_proxy_methods.rb +50 -0
- data/lib/neo4j/active_node/dependent.rb +11 -0
- data/lib/neo4j/active_node/enum.rb +29 -0
- data/lib/neo4j/active_node/has_n/association/rel_factory.rb +61 -0
- data/lib/neo4j/active_node/has_n/association/rel_wrapper.rb +23 -0
- data/lib/neo4j/active_node/has_n/association.rb +280 -0
- data/lib/neo4j/active_node/has_n/association_cypher_methods.rb +108 -0
- data/lib/neo4j/active_node/has_n.rb +532 -0
- data/lib/neo4j/active_node/id_property/accessor.rb +62 -0
- data/lib/neo4j/active_node/id_property.rb +187 -0
- data/lib/neo4j/active_node/initialize.rb +21 -0
- data/lib/neo4j/active_node/labels/index.rb +87 -0
- data/lib/neo4j/active_node/labels/reloading.rb +21 -0
- data/lib/neo4j/active_node/labels.rb +198 -0
- data/lib/neo4j/active_node/node_wrapper.rb +52 -0
- data/lib/neo4j/active_node/orm_adapter.rb +82 -0
- data/lib/neo4j/active_node/persistence.rb +175 -0
- data/lib/neo4j/active_node/property.rb +60 -0
- data/lib/neo4j/active_node/query/query_proxy.rb +361 -0
- data/lib/neo4j/active_node/query/query_proxy_eager_loading.rb +61 -0
- data/lib/neo4j/active_node/query/query_proxy_enumerable.rb +90 -0
- data/lib/neo4j/active_node/query/query_proxy_find_in_batches.rb +19 -0
- data/lib/neo4j/active_node/query/query_proxy_link.rb +117 -0
- data/lib/neo4j/active_node/query/query_proxy_methods.rb +210 -0
- data/lib/neo4j/active_node/query/query_proxy_methods_of_mass_updating.rb +83 -0
- data/lib/neo4j/active_node/query.rb +76 -0
- data/lib/neo4j/active_node/query_methods.rb +65 -0
- data/lib/neo4j/active_node/reflection.rb +86 -0
- data/lib/neo4j/active_node/rels.rb +11 -0
- data/lib/neo4j/active_node/scope.rb +146 -0
- data/lib/neo4j/active_node/unpersisted.rb +48 -0
- data/lib/neo4j/active_node/validations.rb +59 -0
- data/lib/neo4j/active_node.rb +105 -0
- data/lib/neo4j/active_rel/callbacks.rb +15 -0
- data/lib/neo4j/active_rel/initialize.rb +28 -0
- data/lib/neo4j/active_rel/persistence/query_factory.rb +95 -0
- data/lib/neo4j/active_rel/persistence.rb +114 -0
- data/lib/neo4j/active_rel/property.rb +95 -0
- data/lib/neo4j/active_rel/query.rb +95 -0
- data/lib/neo4j/active_rel/rel_wrapper.rb +22 -0
- data/lib/neo4j/active_rel/related_node.rb +83 -0
- data/lib/neo4j/active_rel/types.rb +82 -0
- data/lib/neo4j/active_rel/validations.rb +8 -0
- data/lib/neo4j/active_rel.rb +67 -0
- data/lib/neo4j/class_arguments.rb +39 -0
- data/lib/neo4j/config.rb +124 -0
- data/lib/neo4j/core/query.rb +22 -0
- data/lib/neo4j/errors.rb +28 -0
- data/lib/neo4j/migration.rb +127 -0
- data/lib/neo4j/paginated.rb +27 -0
- data/lib/neo4j/railtie.rb +169 -0
- data/lib/neo4j/schema/operation.rb +91 -0
- data/lib/neo4j/shared/attributes.rb +220 -0
- data/lib/neo4j/shared/callbacks.rb +64 -0
- data/lib/neo4j/shared/cypher.rb +37 -0
- data/lib/neo4j/shared/declared_properties.rb +204 -0
- data/lib/neo4j/shared/declared_property/index.rb +37 -0
- data/lib/neo4j/shared/declared_property.rb +118 -0
- data/lib/neo4j/shared/enum.rb +148 -0
- data/lib/neo4j/shared/filtered_hash.rb +79 -0
- data/lib/neo4j/shared/identity.rb +28 -0
- data/lib/neo4j/shared/initialize.rb +28 -0
- data/lib/neo4j/shared/marshal.rb +23 -0
- data/lib/neo4j/shared/mass_assignment.rb +58 -0
- data/lib/neo4j/shared/permitted_attributes.rb +28 -0
- data/lib/neo4j/shared/persistence.rb +231 -0
- data/lib/neo4j/shared/property.rb +220 -0
- data/lib/neo4j/shared/query_factory.rb +101 -0
- data/lib/neo4j/shared/rel_type_converters.rb +43 -0
- data/lib/neo4j/shared/serialized_properties.rb +30 -0
- data/lib/neo4j/shared/type_converters.rb +418 -0
- data/lib/neo4j/shared/typecasted_attributes.rb +98 -0
- data/lib/neo4j/shared/typecaster.rb +53 -0
- data/lib/neo4j/shared/validations.rb +48 -0
- data/lib/neo4j/shared.rb +51 -0
- data/lib/neo4j/tasks/migration.rake +24 -0
- data/lib/neo4j/timestamps/created.rb +9 -0
- data/lib/neo4j/timestamps/updated.rb +9 -0
- data/lib/neo4j/timestamps.rb +11 -0
- data/lib/neo4j/type_converters.rb +7 -0
- data/lib/neo4j/version.rb +3 -0
- data/lib/neo4j/wrapper.rb +4 -0
- data/lib/neo4j.rb +96 -0
- data/lib/rails/generators/neo4j/model/model_generator.rb +86 -0
- data/lib/rails/generators/neo4j/model/templates/model.erb +15 -0
- data/lib/rails/generators/neo4j_generator.rb +67 -0
- data/neo4j.gemspec +43 -0
- 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
|