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.
@@ -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