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.
@@ -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
- relationship_props = self.class.extract_relationship_attributes!(attributes)
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 extract_relationship_attributes!(attributes)
81
- attributes.keys.inject({}) do |relationship_props, key|
82
- relationship_props[key] = attributes.delete(key) if self.has_relationship?(key)
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
- relationship_props
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
- label = self.respond_to?(:mapped_label_name) ? self.mapped_label_name : self
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
- Neo4j::ActiveNode::Query::QueryProxy.new(self).#{method}(*args)
47
+ self.query_proxy.#{method}(*args)
45
48
  end}, __FILE__, __LINE__)
46
49
  end
47
50
 
48
- def qq(as = :n1)
49
- QuickQuery.new(self.name.constantize, as)
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
- query_as(:n).pluck(:n).each do |obj|
15
- yield obj
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
- query = @model.query_as(var).return(var)
74
+ var = @node_var if @node_var
33
75
 
34
- @chain.inject(query) do |query, (method, arg)|
35
- if arg.respond_to?(:call)
36
- query.send(method, arg.call(var))
37
- else
38
- query.send(method, arg)
39
- end
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
- query_as(:n).to_cypher
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 add_links(links)
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
- args.each do |arg|
58
- new_query.add_links(links_for_arg(method, arg))
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
- def links_for_arg(method, arg)
64
- method_to_call = "links_for_#{method}_arg"
216
+ def links_for_arg(method, arg)
217
+ method_to_call = "links_for_#{method}_arg"
65
218
 
66
- default = [[method, arg]]
219
+ default = [[method, arg]]
67
220
 
68
- self.send(method_to_call, arg) || default
69
- rescue NoMethodError
70
- default
71
- end
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
- def links_for_where_arg(arg)
74
- node_num = 1
75
- result = []
76
- if arg.is_a?(Hash)
77
- arg.map do |key, value|
78
- if @model.has_one_relationship?(key)
79
- neo_id = value.try(:neo_id) || value
80
- raise ArgumentError, "Invalid value for '#{key}' condition" if not neo_id.is_a?(Integer)
81
-
82
- n_string = "n#{node_num}"
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
- def links_for_order_arg(arg)
98
- [[:order, ->(v) { {v => arg} }]]
99
- end
253
+ def links_for_order_arg(arg)
254
+ [[:order, ->(v) { {v => arg} }]]
255
+ end
100
256
 
101
257
 
102
258
  end