neo4j 3.0.0.alpha.8 → 3.0.0.alpha.9
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.
- 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
|