neo4j 3.0.0.alpha.8 → 3.0.0.alpha.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +8 -0
- data/Gemfile +1 -1
- data/config/neo4j/config.yml +19 -84
- data/lib/neo4j.rb +5 -2
- data/lib/neo4j.rb~ +40 -0
- data/lib/neo4j/active_node.rb +19 -1
- data/lib/neo4j/active_node/has_n.rb +64 -124
- data/lib/neo4j/active_node/has_n/association.rb +142 -0
- data/lib/neo4j/active_node/id_property.rb +119 -0
- data/lib/neo4j/active_node/identity.rb +0 -5
- data/lib/neo4j/active_node/initialize.rb +1 -0
- data/lib/neo4j/active_node/labels.rb +62 -25
- data/lib/neo4j/active_node/persistence.rb +26 -5
- data/lib/neo4j/active_node/property.rb +121 -18
- data/lib/neo4j/active_node/query.rb +17 -9
- data/lib/neo4j/active_node/query/query_proxy.rb +202 -46
- data/lib/neo4j/active_node/serialized_properties.rb +21 -0
- data/lib/neo4j/config.rb +119 -0
- data/lib/neo4j/paginated.rb +19 -0
- data/lib/neo4j/railtie.rb +1 -0
- data/lib/neo4j/type_converters.rb +49 -3
- data/lib/neo4j/version.rb +1 -1
- data/lib/neo4j/wrapper.rb +15 -10
- data/lib/person.rb~ +9 -0
- data/lib/rails/generators/neo4j_generator.rb +1 -1
- data/lib/test.rb +21 -0
- data/lib/test.rb~ +21 -0
- data/neo4j.gemspec +2 -1
- metadata +27 -5
- data/lib/neo4j/active_node/has_n/decl_rel.rb +0 -252
@@ -9,11 +9,12 @@ module Neo4j::ActiveNode
|
|
9
9
|
include ActiveAttr::QueryAttributes
|
10
10
|
include ActiveModel::Dirty
|
11
11
|
|
12
|
-
class UndefinedPropertyError < RuntimeError
|
13
|
-
end
|
12
|
+
class UndefinedPropertyError < RuntimeError; end
|
13
|
+
class MultiparameterAssignmentError < StandardError; end
|
14
14
|
|
15
15
|
def initialize(attributes={}, options={})
|
16
|
-
|
16
|
+
attributes = process_attributes(attributes)
|
17
|
+
|
17
18
|
writer_method_props = extract_writer_methods!(attributes)
|
18
19
|
validate_attributes!(attributes)
|
19
20
|
writer_method_props.each do |key, value|
|
@@ -23,12 +24,6 @@ module Neo4j::ActiveNode
|
|
23
24
|
super(attributes, options)
|
24
25
|
end
|
25
26
|
|
26
|
-
def save_properties
|
27
|
-
@previously_changed = changes
|
28
|
-
changed_attributes.clear
|
29
|
-
end
|
30
|
-
|
31
|
-
|
32
27
|
# Returning nil when we get ActiveAttr::UnknownAttributeError from ActiveAttr
|
33
28
|
def read_attribute(name)
|
34
29
|
super(name)
|
@@ -37,6 +32,23 @@ module Neo4j::ActiveNode
|
|
37
32
|
end
|
38
33
|
alias_method :[], :read_attribute
|
39
34
|
|
35
|
+
def default_properties=(properties)
|
36
|
+
keys = self.class.default_properties.keys
|
37
|
+
@default_properties = properties.reject{|key| !keys.include?(key)}
|
38
|
+
end
|
39
|
+
|
40
|
+
def default_property(key)
|
41
|
+
keys = self.class.default_properties.keys
|
42
|
+
keys.include?(key.to_sym) ? default_properties[key.to_sym] : nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_properties
|
46
|
+
@default_properties ||= {}
|
47
|
+
# keys = self.class.default_properties.keys
|
48
|
+
# _persisted_node.props.reject{|key| !keys.include?(key)}
|
49
|
+
end
|
50
|
+
|
51
|
+
|
40
52
|
private
|
41
53
|
|
42
54
|
# Changes attributes hash to remove relationship keys
|
@@ -54,18 +66,105 @@ module Neo4j::ActiveNode
|
|
54
66
|
end
|
55
67
|
end
|
56
68
|
|
69
|
+
# Gives support for Rails date_select, datetime_select, time_select helpers.
|
70
|
+
def process_attributes(attributes = nil)
|
71
|
+
multi_parameter_attributes = {}
|
72
|
+
new_attributes = {}
|
73
|
+
attributes.each_pair do |key, value|
|
74
|
+
if key =~ /\A([^\(]+)\((\d+)([if])\)$/
|
75
|
+
found_key, index = $1, $2.to_i
|
76
|
+
(multi_parameter_attributes[found_key] ||= {})[index] = value.empty? ? nil : value.send("to_#{$3}")
|
77
|
+
else
|
78
|
+
new_attributes[key] = value
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
multi_parameter_attributes.empty? ? new_attributes : process_multiparameter_attributes(multi_parameter_attributes, new_attributes)
|
83
|
+
end
|
84
|
+
|
85
|
+
def process_multiparameter_attributes(multi_parameter_attributes, new_attributes)
|
86
|
+
multi_parameter_attributes.each_pair do |key, values|
|
87
|
+
begin
|
88
|
+
values = (values.keys.min..values.keys.max).map { |i| values[i] }
|
89
|
+
field = self.class.attributes[key.to_sym]
|
90
|
+
new_attributes[key] = instantiate_object(field, values)
|
91
|
+
rescue => e
|
92
|
+
raise MultiparameterAssignmentError, "error on assignment #{values.inspect} to #{key}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
new_attributes
|
96
|
+
end
|
97
|
+
|
98
|
+
def instantiate_object(field, values_with_empty_parameters)
|
99
|
+
return nil if values_with_empty_parameters.all? { |v| v.nil? }
|
100
|
+
values = values_with_empty_parameters.collect { |v| v.nil? ? 1 : v }
|
101
|
+
klass = field[:type]
|
102
|
+
if klass
|
103
|
+
klass.new(*values)
|
104
|
+
else
|
105
|
+
values
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
57
109
|
module ClassMethods
|
58
110
|
|
111
|
+
# Defines a property on the class
|
112
|
+
#
|
113
|
+
# See active_attr gem for allowed options, e.g which type
|
114
|
+
# Notice, in Neo4j you don't have to declare properties before using them, see the neo4j-core api.
|
115
|
+
#
|
116
|
+
# @example Without type
|
117
|
+
# class Person
|
118
|
+
# # declare a property which can have any value
|
119
|
+
# property :name
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# @example With type and a default value
|
123
|
+
# class Person
|
124
|
+
# # declare a property which can have any value
|
125
|
+
# property :score, type: Integer, default: 0
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# @example With an index
|
129
|
+
# class Person
|
130
|
+
# # declare a property which can have any value
|
131
|
+
# property :name, index: :exact
|
132
|
+
# end
|
133
|
+
#
|
134
|
+
# @example With a constraint
|
135
|
+
# class Person
|
136
|
+
# # declare a property which can have any value
|
137
|
+
# property :name, constraint: :unique
|
138
|
+
# end
|
59
139
|
def property(name, options={})
|
60
140
|
magic_properties(name, options)
|
61
|
-
|
62
|
-
# if (name.to_s == 'remember_created_at')
|
63
|
-
# binding.pry
|
64
|
-
# end
|
65
141
|
attribute(name, options)
|
142
|
+
|
143
|
+
# either constraint or index, do not set both
|
144
|
+
if options[:constraint]
|
145
|
+
raise "unknown constraint type #{options[:constraint]}, only :unique supported" if options[:constraint] != :unique
|
146
|
+
constraint(name, type: :unique)
|
147
|
+
elsif options[:index]
|
148
|
+
raise "unknown index type #{options[:index]}, only :exact supported" if options[:index] != :exact
|
149
|
+
index(name, options) if options[:index] == :exact
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def default_property(name, &block)
|
154
|
+
default_properties[name] = block
|
155
|
+
end
|
156
|
+
|
157
|
+
# @return [Hash<Symbol,Proc>]
|
158
|
+
def default_properties
|
159
|
+
@default_property ||= {}
|
160
|
+
end
|
161
|
+
|
162
|
+
def default_property_values(instance)
|
163
|
+
default_properties.inject({}) do |result,pair|
|
164
|
+
result.tap{|obj| obj[pair[0]] = pair[1].call(instance)}
|
165
|
+
end
|
66
166
|
end
|
67
167
|
|
68
|
-
#overrides ActiveAttr's attribute! method
|
69
168
|
def attribute!(name, options={})
|
70
169
|
super(name, options)
|
71
170
|
define_method("#{name}=") do |value|
|
@@ -75,13 +174,17 @@ module Neo4j::ActiveNode
|
|
75
174
|
end
|
76
175
|
end
|
77
176
|
|
177
|
+
def cached_class?
|
178
|
+
!!Neo4j::Config[:cache_class_names]
|
179
|
+
end
|
180
|
+
|
78
181
|
# Extracts keys from attributes hash which are relationships of the model
|
79
182
|
# TODO: Validate separately that relationships are getting the right values? Perhaps also store the values and persist relationships on save?
|
80
|
-
def
|
81
|
-
attributes.keys.inject({}) do |
|
82
|
-
|
183
|
+
def extract_association_attributes!(attributes)
|
184
|
+
attributes.keys.inject({}) do |association_props, key|
|
185
|
+
association_props[key] = attributes.delete(key) if self.has_association?(key)
|
83
186
|
|
84
|
-
|
187
|
+
association_props
|
85
188
|
end
|
86
189
|
end
|
87
190
|
|
@@ -1,10 +1,6 @@
|
|
1
1
|
module Neo4j
|
2
2
|
module ActiveNode
|
3
3
|
|
4
|
-
def qq(as = :n1)
|
5
|
-
QuickQuery.new(self, as, self.class)
|
6
|
-
end
|
7
|
-
|
8
4
|
# Helper methods to return Neo4j::Core::Query objects. A query object can be used to successively build a cypher query
|
9
5
|
#
|
10
6
|
# person.query_as(:n).match('n-[:friend]-o').return(o: :name) # Return the names of all the person's friends
|
@@ -25,6 +21,14 @@ module Neo4j
|
|
25
21
|
end
|
26
22
|
|
27
23
|
module ClassMethods
|
24
|
+
include Enumerable
|
25
|
+
|
26
|
+
attr_writer :query_proxy
|
27
|
+
|
28
|
+
def each
|
29
|
+
self.query_as(:n).pluck(:n).each {|o| yield o }
|
30
|
+
end
|
31
|
+
|
28
32
|
# Returns a Query object with all nodes for the model matched as the specified variable name
|
29
33
|
#
|
30
34
|
# @example Return the registration number of all cars owned by a person over the age of 30
|
@@ -34,20 +38,24 @@ module Neo4j
|
|
34
38
|
# @param var [Symbol, String] The variable name to specify in the query
|
35
39
|
# @return [Neo4j::Core::Query]
|
36
40
|
def query_as(var)
|
37
|
-
|
38
|
-
neo4j_session.query.match(var => label)
|
41
|
+
query_proxy.query_as(var)
|
39
42
|
end
|
40
43
|
|
41
44
|
Neo4j::ActiveNode::Query::QueryProxy::METHODS.each do |method|
|
42
45
|
module_eval(%Q{
|
43
46
|
def #{method}(*args)
|
44
|
-
|
47
|
+
self.query_proxy.#{method}(*args)
|
45
48
|
end}, __FILE__, __LINE__)
|
46
49
|
end
|
47
50
|
|
48
|
-
def
|
49
|
-
|
51
|
+
def query_proxy(options = {})
|
52
|
+
@query_proxy || Neo4j::ActiveNode::Query::QueryProxy.new(self, nil, options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def as(node_var)
|
56
|
+
query_proxy(node: node_var)
|
50
57
|
end
|
58
|
+
|
51
59
|
end
|
52
60
|
end
|
53
61
|
end
|
@@ -5,17 +5,42 @@ module Neo4j
|
|
5
5
|
class QueryProxy
|
6
6
|
include Enumerable
|
7
7
|
|
8
|
-
def initialize(model)
|
8
|
+
def initialize(model, association = nil, options = {})
|
9
9
|
@model = model
|
10
|
+
@association = association
|
11
|
+
@options = options
|
12
|
+
@node_var = options[:node]
|
13
|
+
@rel_var = options[:rel] || _rel_chain_var
|
14
|
+
@session = options[:session]
|
10
15
|
@chain = []
|
16
|
+
@params = {}
|
11
17
|
end
|
12
18
|
|
13
|
-
def each
|
14
|
-
|
15
|
-
|
19
|
+
def each(node = true, rel = nil, &block)
|
20
|
+
if node && rel
|
21
|
+
self.pluck((@node_var || :result), @rel_var).each do |obj, rel|
|
22
|
+
yield obj, rel
|
23
|
+
end
|
24
|
+
else
|
25
|
+
pluck_this = !rel ? (@node_var || :result) : @rel_var
|
26
|
+
self.pluck(pluck_this).each do |obj|
|
27
|
+
yield obj
|
28
|
+
end
|
16
29
|
end
|
17
30
|
end
|
18
31
|
|
32
|
+
def each_rel(&block)
|
33
|
+
block_given? ? each(false, true, &block) : to_enum(:each, false, true)
|
34
|
+
end
|
35
|
+
|
36
|
+
def each_with_rel(&block)
|
37
|
+
block_given? ? each(true, true, &block) : to_enum(:each, true, true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(value)
|
41
|
+
self.to_a == value
|
42
|
+
end
|
43
|
+
|
19
44
|
METHODS = %w[where order skip limit]
|
20
45
|
|
21
46
|
METHODS.each do |method|
|
@@ -28,75 +53,206 @@ module Neo4j
|
|
28
53
|
alias_method :offset, :skip
|
29
54
|
alias_method :order_by, :order
|
30
55
|
|
56
|
+
# For getting variables which have been defined as part of the association chain
|
57
|
+
def pluck(*args)
|
58
|
+
self.query.pluck(*args)
|
59
|
+
end
|
60
|
+
|
61
|
+
def params(params)
|
62
|
+
self.dup.tap do |new_query|
|
63
|
+
new_query._add_params(params)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Like calling #query_as, but for when you don't care about the variable name
|
68
|
+
def query
|
69
|
+
query_as(@node_var || :result)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Build a Neo4j::Core::Query object for the QueryProxy
|
31
73
|
def query_as(var)
|
32
|
-
|
74
|
+
var = @node_var if @node_var
|
33
75
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
76
|
+
query = if @association
|
77
|
+
chain_var = _association_chain_var
|
78
|
+
label_string = @model && ":`#{@model.name}`"
|
79
|
+
(_association_query_start(chain_var) & _query_model_as(var)).match("#{chain_var}#{_association_arrow}(#{var}#{label_string})")
|
80
|
+
else
|
81
|
+
_query_model_as(var)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Build a query chain via the chain, return the result
|
85
|
+
@chain.inject(query.params(@params)) do |query, (method, arg)|
|
86
|
+
query.send(method, arg.respond_to?(:call) ? arg.call(var) : arg)
|
40
87
|
end
|
41
88
|
end
|
42
89
|
|
90
|
+
# Cypher string for the QueryProxy's query
|
43
91
|
def to_cypher
|
44
|
-
|
92
|
+
query.to_cypher
|
93
|
+
end
|
94
|
+
|
95
|
+
# To add a relationship for the node for the association on this QueryProxy
|
96
|
+
def <<(other_node)
|
97
|
+
create(other_node, {})
|
98
|
+
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
def [](index)
|
103
|
+
# TODO: Maybe for this and other methods, use array if already loaded, otherwise
|
104
|
+
# use OFFSET and LIMIT 1?
|
105
|
+
self.to_a[index]
|
106
|
+
end
|
107
|
+
|
108
|
+
def create(other_nodes, properties)
|
109
|
+
raise "Can only create associations on associations" unless @association
|
110
|
+
other_nodes = [other_nodes].flatten
|
111
|
+
|
112
|
+
raise ArgumentError, "Node must be of the association's class when model is specified" if @model && other_nodes.any? {|other_node| other_node.class != @model }
|
113
|
+
other_nodes.each do |other_node|
|
114
|
+
#Neo4j::Transaction.run do
|
115
|
+
other_node.save if not other_node.persisted?
|
116
|
+
|
117
|
+
return false if @association.perform_callback(@options[:start_object], other_node, :before) == false
|
118
|
+
|
119
|
+
_association_query_start(:start)
|
120
|
+
.match(end: other_node.class)
|
121
|
+
.where(end: {neo_id: other_node.neo_id})
|
122
|
+
.create("start#{_association_arrow(properties, true)}end").exec
|
123
|
+
|
124
|
+
@association.perform_callback(@options[:start_object], other_node, :after)
|
125
|
+
#end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# QueryProxy objects act as a representation of a model at the class level so we pass through calls
|
130
|
+
# This allows us to define class functions for reusable query chaining or for end-of-query aggregation/summarizing
|
131
|
+
def method_missing(method_name, *args)
|
132
|
+
if @model && @model.respond_to?(method_name)
|
133
|
+
@model.query_proxy = self
|
134
|
+
result = @model.send(method_name, *args)
|
135
|
+
@model.query_proxy = nil
|
136
|
+
result
|
137
|
+
else
|
138
|
+
super
|
139
|
+
end
|
45
140
|
end
|
46
141
|
|
47
142
|
protected
|
143
|
+
# Methods are underscored to prevent conflict with user class methods
|
144
|
+
|
145
|
+
attr_reader :node_var
|
146
|
+
|
147
|
+
def _add_params(params)
|
148
|
+
@params = @params.merge(params)
|
149
|
+
end
|
48
150
|
|
49
|
-
def
|
151
|
+
def _add_links(links)
|
50
152
|
@chain += links
|
51
153
|
end
|
52
154
|
|
155
|
+
def _query_model_as(var)
|
156
|
+
if @model
|
157
|
+
label = @model.respond_to?(:mapped_label_name) ? @model.mapped_label_name : @model
|
158
|
+
_session.query.match(var => label)
|
159
|
+
else
|
160
|
+
_session.query.match(var)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def _session
|
165
|
+
@session || (@model && @model.neo4j_session)
|
166
|
+
end
|
167
|
+
|
168
|
+
def _association_arrow(properties = {}, create = false)
|
169
|
+
@association && @association.arrow_cypher(@rel_var, properties, create)
|
170
|
+
end
|
171
|
+
|
172
|
+
def _chain_level
|
173
|
+
if @options[:start_object]
|
174
|
+
1
|
175
|
+
elsif query_proxy = @options[:query_proxy]
|
176
|
+
query_proxy._chain_level + 1
|
177
|
+
else
|
178
|
+
1
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def _association_chain_var
|
183
|
+
if start_object = @options[:start_object]
|
184
|
+
:"#{start_object.class.name.gsub('::', '_').downcase}#{start_object.neo_id}"
|
185
|
+
elsif query_proxy = @options[:query_proxy]
|
186
|
+
query_proxy.node_var || :"node#{_chain_level}"
|
187
|
+
else
|
188
|
+
raise "Crazy error" # TODO: Better error
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def _association_query_start(var)
|
193
|
+
if start_object = @options[:start_object]
|
194
|
+
start_object.query_as(var)
|
195
|
+
elsif query_proxy = @options[:query_proxy]
|
196
|
+
query_proxy.query_as(var)
|
197
|
+
else
|
198
|
+
raise "Crazy error" # TODO: Better error
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def _rel_chain_var
|
203
|
+
:"rel#{_chain_level - 1}"
|
204
|
+
end
|
205
|
+
|
53
206
|
private
|
54
207
|
|
55
208
|
def build_deeper_query_proxy(method, args)
|
56
209
|
self.dup.tap do |new_query|
|
57
|
-
|
58
|
-
|
210
|
+
args.each do |arg|
|
211
|
+
new_query._add_links(links_for_arg(method, arg))
|
212
|
+
end
|
59
213
|
end
|
60
214
|
end
|
61
|
-
end
|
62
215
|
|
63
|
-
|
64
|
-
|
216
|
+
def links_for_arg(method, arg)
|
217
|
+
method_to_call = "links_for_#{method}_arg"
|
65
218
|
|
66
|
-
|
219
|
+
default = [[method, arg]]
|
67
220
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
221
|
+
self.send(method_to_call, arg) || default
|
222
|
+
rescue NoMethodError
|
223
|
+
default
|
224
|
+
end
|
225
|
+
|
226
|
+
def links_for_where_arg(arg)
|
227
|
+
node_num = 1
|
228
|
+
result = []
|
229
|
+
if arg.is_a?(Hash)
|
230
|
+
arg.map do |key, value|
|
231
|
+
if @model && @model.has_association?(key)
|
232
|
+
|
233
|
+
neo_id = value.try(:neo_id) || value
|
234
|
+
raise ArgumentError, "Invalid value for '#{key}' condition" if not neo_id.is_a?(Integer)
|
72
235
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
dir = @model.relationship_dir(key)
|
84
|
-
|
85
|
-
arrow = dir == :outgoing ? '-->' : '<--'
|
86
|
-
result << [:match, ->(v) { "#{v}#{arrow}(#{n_string})" }]
|
87
|
-
result << [:where, ->(v) { {"ID(#{n_string})" => neo_id.to_i} }]
|
88
|
-
node_num += 1
|
89
|
-
else
|
90
|
-
result << [:where, ->(v) { {v => {key => value}}}]
|
236
|
+
n_string = "n#{node_num}"
|
237
|
+
dir = @model.associations[key].direction
|
238
|
+
|
239
|
+
arrow = dir == :out ? '-->' : '<--'
|
240
|
+
result << [:match, ->(v) { "#{v}#{arrow}(#{n_string})" }]
|
241
|
+
result << [:where, ->(v) { {"ID(#{n_string})" => neo_id.to_i} }]
|
242
|
+
node_num += 1
|
243
|
+
else
|
244
|
+
result << [:where, ->(v) { {v => {key => value}}}]
|
245
|
+
end
|
91
246
|
end
|
247
|
+
elsif arg.is_a?(String)
|
248
|
+
result << [:where, arg]
|
92
249
|
end
|
250
|
+
result
|
93
251
|
end
|
94
|
-
result
|
95
|
-
end
|
96
252
|
|
97
|
-
|
98
|
-
|
99
|
-
|
253
|
+
def links_for_order_arg(arg)
|
254
|
+
[[:order, ->(v) { {v => arg} }]]
|
255
|
+
end
|
100
256
|
|
101
257
|
|
102
258
|
end
|