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.
@@ -0,0 +1,142 @@
1
+ require 'active_support/inflector/inflections'
2
+
3
+ module Neo4j
4
+ module ActiveNode
5
+ module HasN
6
+ class Association
7
+ attr_reader :type, :name, :relationship, :direction
8
+
9
+ def initialize(type, direction, name, options = {})
10
+ raise ArgumentError, "Invalid association type: #{type.inspect}" if not [:has_many, :has_one].include?(type.to_sym)
11
+ raise ArgumentError, "Invalid direction: #{direction.inspect}" if not [:out, :in, :both].include?(direction.to_sym)
12
+ @type = type.to_sym
13
+ @name = name
14
+ @direction = direction.to_sym
15
+ raise ArgumentError, "Cannot specify both :type and :origin (#{base_declaration})" if options[:type] && options[:origin]
16
+
17
+ @target_class_name_from_name = name.to_s.classify
18
+ @target_class_option = target_class_option(options)
19
+ @callbacks = {before: options[:before], after: options[:after]}
20
+ @relationship_type = options[:type] && options[:type].to_sym
21
+ @origin = options[:origin] && options[:origin].to_sym
22
+ end
23
+
24
+ def target_class_option(options)
25
+ if options[:model_class].nil?
26
+ @target_class_name_from_name
27
+ elsif options[:model_class]
28
+ options[:model_class]
29
+ end
30
+ end
31
+
32
+ # Return cypher partial query string for the relationship part of a MATCH (arrow / relationship definition)
33
+ def arrow_cypher(var = nil, properties = {}, create = false)
34
+ validate_origin!
35
+
36
+ relationship_type = relationship_type(create)
37
+ relationship_name_cypher = ":`#{relationship_type}`" if relationship_type
38
+
39
+ properties_string = get_properties_string(properties)
40
+ relationship_cypher = get_relationship_cypher(var, relationship_name_cypher, properties_string)
41
+ get_direction(relationship_cypher, create)
42
+ end
43
+
44
+ def target_class_name
45
+ @target_class_option.to_s if @target_class_option
46
+ end
47
+
48
+ def target_class
49
+ return @target_class if @target_class
50
+
51
+ @target_class = target_class_name.constantize if target_class_name
52
+ rescue NameError
53
+ raise ArgumentError, "Could not find `#{@target_class}` class and no :model_class specified"
54
+ end
55
+
56
+ def callback(type)
57
+ @callbacks[type]
58
+ end
59
+
60
+ def perform_callback(caller, other_node, type)
61
+ return if callback(type).nil?
62
+ caller.send(callback(type), other_node)
63
+ end
64
+
65
+ def relationship_type(create = false)
66
+ if @relationship_type
67
+ @relationship_type
68
+ elsif @origin
69
+ origin_type
70
+ else
71
+ (create || exceptional_target_class?) && "##{@name}"
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def get_direction(relationship_cypher, create)
78
+ dir = (create && @direction == :both) ? :out : @direction
79
+ case dir
80
+ when :out
81
+ "-#{relationship_cypher}->"
82
+ when :in
83
+ "<-#{relationship_cypher}-"
84
+ when :both
85
+ "-#{relationship_cypher}-"
86
+ end
87
+ end
88
+
89
+ def get_relationship_cypher(var, relationship_name_cypher, properties_string)
90
+ "[#{var}#{relationship_name_cypher}#{properties_string}]"
91
+ end
92
+
93
+ def get_properties_string(properties)
94
+ p = properties.map do |key, value|
95
+ "#{key}: #{value.inspect}"
96
+ end.join(', ')
97
+ p.size == 0 ? '' : " {#{p}}"
98
+ end
99
+
100
+ def origin_type
101
+ target_class.associations[@origin].relationship_type
102
+ end
103
+
104
+ # Return basic details about association as declared in the model
105
+ # @example
106
+ # has_many :in, :bands
107
+ def base_declaration
108
+ "#{type} #{direction.inspect}, #{name.inspect}"
109
+ end
110
+
111
+
112
+ # Determine if model class as derived from the association name would be different than the one specified via the model_class key
113
+ # @example
114
+ # has_many :friends # Would return false
115
+ # has_many :friends, model_class: Friend # Would return false
116
+ # has_many :friends, model_class: Person # Would return true
117
+ def exceptional_target_class?
118
+ # TODO: Exceptional if target_class.nil?? (when model_class false)
119
+
120
+ target_class && target_class.name != @target_class_name_from_name
121
+ end
122
+
123
+ def validate_origin!
124
+ if @origin
125
+ if target_class
126
+ if association = target_class.associations[@origin]
127
+ if @direction == association.direction
128
+ raise ArgumentError, "Origin `#{@origin.inspect}` (specified in #{base_declaration}) has same direction `#{@direction}`)"
129
+ end
130
+ else
131
+ raise ArgumentError, "Origin `#{@origin.inspect}` association not found for #{target_class} (specified in #{base_declaration})"
132
+ end
133
+ else
134
+ raise ArgumentError, "Cannot use :origin without a model_class (implied or explicit)"
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+
@@ -0,0 +1,119 @@
1
+ module Neo4j::ActiveNode
2
+
3
+ # This module makes it possible to use other IDs than the build it neo4j id (neo_id)
4
+ #
5
+ # @example using generated UUIDs
6
+ # class Person
7
+ # include Neo4j::ActiveNode
8
+ # # creates a 'secret' neo4j property my_uuid which will be used as primary key
9
+ # id_property :my_uuid, auto: :uuid
10
+ # end
11
+ #
12
+ # @example using user defined ids
13
+ # class Person
14
+ # include Neo4j::ActiveNode
15
+ # property :title
16
+ # validates :title, :presence => true
17
+ # id_property :title_id, on: :title_to_url
18
+ #
19
+ # def title_to_url
20
+ # self.title.urlize # uses https://github.com/cheef/string-urlize gem
21
+ # end
22
+ # end
23
+ #
24
+ module IdProperty
25
+ extend ActiveSupport::Concern
26
+
27
+
28
+ module TypeMethods
29
+ def define_id_methods(clazz, name, conf)
30
+ validate_conf(conf)
31
+ if conf[:on]
32
+ define_custom_method(clazz, name, conf[:on])
33
+ elsif conf[:auto]
34
+ raise "only :uuid auto id_property allowed, got #{conf[:auto]}" unless conf[:auto] == :uuid
35
+ define_uuid_method(clazz, name)
36
+ else conf.empty?
37
+ define_property_method(clazz, name)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def validate_conf(conf)
44
+ return if conf.empty?
45
+
46
+ raise "Expected a Hash, got #{conf.class} (#{conf.to_s}) for id_property" unless conf.is_a?(Hash)
47
+
48
+ unless conf.include?(:auto) || conf.include?(:on)
49
+ raise "Illegal value #{conf.inspect} for id_property, expected :on or :auto"
50
+ end
51
+ end
52
+
53
+ def define_property_method(clazz, name)
54
+ clazz.module_eval(%Q{
55
+ def id
56
+ persisted? ? #{name} : nil
57
+ end
58
+
59
+ property :#{name}
60
+ validates_uniqueness_of :#{name}
61
+ }, __FILE__, __LINE__)
62
+ end
63
+
64
+
65
+ def define_uuid_method(clazz, name)
66
+ clazz.module_eval(%Q{
67
+ default_property :#{name} do
68
+ ::SecureRandom.uuid
69
+ end
70
+
71
+ def #{name}
72
+ default_property :#{name}
73
+ end
74
+
75
+ alias_method :id, :#{name}
76
+ }, __FILE__, __LINE__)
77
+ end
78
+
79
+ def define_custom_method(clazz, name, on)
80
+ clazz.module_eval(%Q{
81
+ default_property :#{name} do |instance|
82
+ raise "Specifying custom id_property #{name} on none existing method #{on}" unless instance.respond_to?(:#{on})
83
+ instance.#{on}
84
+ end
85
+
86
+ def #{name}
87
+ default_property :#{name}
88
+ end
89
+
90
+ alias_method :id, :#{name}
91
+ }, __FILE__, __LINE__)
92
+ end
93
+
94
+ extend self
95
+ end
96
+
97
+
98
+ module ClassMethods
99
+
100
+ def find_by_id(key)
101
+ Neo4j::Node.load(key.to_i)
102
+ end
103
+
104
+
105
+ def id_property(name, conf = {})
106
+ TypeMethods.define_id_methods(self, name, conf)
107
+
108
+ constraint name, type: :unique
109
+
110
+ self.define_singleton_method(:find_by_id) do |key|
111
+ self.where(name => key).first
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+
119
+ end
@@ -1,6 +1,5 @@
1
1
  module Neo4j::ActiveNode
2
2
  module Identity
3
-
4
3
  def ==(o)
5
4
  o.class == self.class && o.id == id
6
5
  end
@@ -17,13 +16,9 @@ module Neo4j::ActiveNode
17
16
  _persisted_node ? _persisted_node.neo_id : nil
18
17
  end
19
18
 
20
- # @return [String, nil] same as #neo_id
21
19
  def id
22
20
  id = neo_id
23
21
  id.is_a?(Integer) ? id : nil
24
22
  end
25
-
26
-
27
23
  end
28
-
29
24
  end
@@ -11,6 +11,7 @@ module Neo4j::ActiveNode::Initialize
11
11
  @_persisted_node = persisted_node
12
12
  changed_attributes && changed_attributes.clear
13
13
  @attributes = attributes.merge(properties.stringify_keys)
14
+ self.default_properties=properties
14
15
  @attributes = convert_properties_to :ruby, @attributes
15
16
  end
16
17
 
@@ -59,20 +59,19 @@ module Neo4j
59
59
  end
60
60
 
61
61
  module ClassMethods
62
-
63
62
  # Find all nodes/objects of this class
64
63
  def all
65
64
  self.query_as(:n).pluck(:n)
66
65
  end
67
66
 
67
+ # Returns the first node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
68
68
  def first
69
- self.query_as(:n).limit(1).order('n.neo_id').pluck(:n).first
69
+ self.query_as(:n).limit(1).order('ID(n)').pluck(:n).first
70
70
  end
71
71
 
72
+ # Returns the last node of this class, sorted by ID. Note that this may not be the first node created since Neo4j recycles IDs.
72
73
  def last
73
- count = self.count
74
- final_count = count == 0 ? 0 : count - 1
75
- self.query_as(:n).order('n.neo_id').skip(final_count).limit(1).pluck(:n).first
74
+ self.query_as(:n).order('ID(n) DESC').limit(1).pluck(:n).first
76
75
  end
77
76
 
78
77
  # @return [Fixnum] number of nodes of this class
@@ -81,15 +80,14 @@ module Neo4j
81
80
  end
82
81
 
83
82
  # Returns the object with the specified neo4j id.
84
- # @param [String,Fixnum] neo_id of node to find
83
+ # @param [String,Fixnum] id of node to find
85
84
  def find(id)
86
- raise "Unknown argument #{id.class} in find method" if not [String, Fixnum].include?(id.class)
87
-
88
- Neo4j::Node.load(id.to_i)
85
+ raise "Unknown argument #{id.class} in find method (expected String or Fixnum)" if not [String, Fixnum].include?(id.class)
86
+ find_by_id(id)
89
87
  end
90
88
 
91
89
  # Finds the first record matching the specified conditions. There is no implied ordering so if order matters, you should specify it yourself.
92
- # @param [Hash] hash of arguments to find
90
+ # @param [Hash] args of arguments to find
93
91
  def find_by(*args)
94
92
  self.query_as(:n).where(n: eval(args.join)).limit(1).pluck(:n).first
95
93
  end
@@ -100,21 +98,51 @@ module Neo4j
100
98
  find_by(args) or raise RecordNotFound, "#{self.query_as(:n).where(n: a).limit(1).to_cypher} returned no results"
101
99
  end
102
100
 
103
- # Destroy all nodes an connected relationships
101
+ # Destroy all nodes and connected relationships
104
102
  def destroy_all
105
103
  self.neo4j_session._query("MATCH (n:`#{mapped_label_name}`)-[r]-() DELETE n,r")
106
104
  self.neo4j_session._query("MATCH (n:`#{mapped_label_name}`) DELETE n")
107
105
  end
108
106
 
109
107
  # Creates a Neo4j index on given property
108
+ #
109
+ # This can also be done on the property directly, see Neo4j::ActiveNode::Property::ClassMethods#property.
110
+ #
110
111
  # @param [Symbol] property the property we want a Neo4j index on
111
- def index(property)
112
- if self.neo4j_session
113
- _index(property)
114
- else
115
- Neo4j::Session.add_listener do |event, _|
116
- _index(property) if event == :session_available
117
- end
112
+ # @param [Hash] conf optional property configuration
113
+ #
114
+ # @example
115
+ # class Person
116
+ # include Neo4j::ActiveNode
117
+ # property :name
118
+ # index :name
119
+ # end
120
+ #
121
+ # @example with constraint
122
+ # class Person
123
+ # include Neo4j::ActiveNode
124
+ # property :name
125
+ #
126
+ # # below is same as: index :name, index: :exact, constraint: {type: :unique}
127
+ # index :name, constraint: {type: :unique}
128
+ # end
129
+ def index(property, conf = {})
130
+ Neo4j::Session.on_session_available do |_|
131
+ _index(property, conf)
132
+ end
133
+ @_indexed_properties ||= []
134
+ @_indexed_properties.push property unless @_indexed_properties.include? property
135
+ end
136
+
137
+ # Creates a neo4j constraint on this class for given property
138
+ #
139
+ # @example
140
+ # Person.constraint :name, type: :unique
141
+ #
142
+ def constraint(property, constraints, session = Neo4j::Session.current)
143
+ Neo4j::Session.on_session_available do |_|
144
+ label = Neo4j::Label.create(mapped_label_name)
145
+ label.create_constraint(property, constraints, session)
118
146
  end
119
147
  end
120
148
 
@@ -132,17 +160,30 @@ module Neo4j
132
160
  @_label_name || self.to_s.to_sym
133
161
  end
134
162
 
135
- def indexed_labels
163
+ # @return [Neo4j::Label] the label for this class
164
+ def mapped_label
165
+ Neo4j::Label.create(mapped_label_name)
166
+ end
136
167
 
168
+ def indexed_properties
169
+ @_indexed_properties
137
170
  end
138
171
 
172
+
139
173
  protected
140
174
 
141
- def _index(property)
175
+ def _index(property, conf)
142
176
  mapped_labels.each do |label|
143
177
  # make sure the property is not indexed twice
144
178
  existing = label.indexes[:property_keys]
145
- label.create_index(property) unless existing.flatten.include?(property)
179
+
180
+ # In neo4j constraint automatically creates an index
181
+ if conf[:constraint]
182
+ constraint(property, conf[:constraint])
183
+ else
184
+ label.create_index(property) unless existing.flatten.include?(property)
185
+ end
186
+
146
187
  end
147
188
  end
148
189
 
@@ -150,10 +191,6 @@ module Neo4j
150
191
  mapped_label_names.map{|label_name| Neo4j::Label.create(label_name)}
151
192
  end
152
193
 
153
- def mapped_label
154
- Neo4j::Label.create(mapped_label_name)
155
- end
156
-
157
194
  def set_mapped_label_name(name)
158
195
  @_label_name = name.to_sym
159
196
  end
@@ -28,6 +28,7 @@ module Neo4j::ActiveNode
28
28
  # @return true
29
29
  def create_model(*)
30
30
  set_timestamps
31
+ create_magic_properties
31
32
  properties = convert_properties_to :db, props
32
33
  node = _create_node(properties)
33
34
  init_on_load(node, node.props)
@@ -129,7 +130,9 @@ module Neo4j::ActiveNode
129
130
 
130
131
  def _create_node(*args)
131
132
  session = self.class.neo4j_session
132
- props = args[0] if args[0].is_a?(Hash)
133
+ props = self.class.default_property_values(self)
134
+ props.merge!(args[0]) if args[0].is_a?(Hash)
135
+ set_classname(props)
133
136
  labels = self.class.mapped_label_names
134
137
  session.create_node(props, labels)
135
138
  end
@@ -193,15 +196,25 @@ module Neo4j::ActiveNode
193
196
  end
194
197
  alias_method :update_attributes!, :update!
195
198
 
199
+ def cache_key
200
+ if self.new_record?
201
+ "#{self.class.model_name.cache_key}/new"
202
+ elsif self.respond_to?(:updated_at) && !self.updated_at.blank?
203
+ "#{self.class.model_name.cache_key}/#{neo_id}-#{self.updated_at.utc.to_s(:number)}"
204
+ else
205
+ "#{self.class.model_name.cache_key}/#{neo_id}"
206
+ end
207
+ end
208
+
196
209
  module ClassMethods
197
210
  # Creates a saves a new node
198
211
  # @param [Hash] props the properties the new node should have
199
212
  def create(props = {})
200
- relationship_props = extract_relationship_attributes!(props)
213
+ association_props = extract_association_attributes!(props)
201
214
 
202
215
  new(props).tap do |obj|
203
216
  obj.save
204
- relationship_props.each do |prop, value|
217
+ association_props.each do |prop, value|
205
218
  obj.send("#{prop}=", value)
206
219
  end
207
220
  end
@@ -210,12 +223,12 @@ module Neo4j::ActiveNode
210
223
  # Same as #create, but raises an error if there is a problem during save.
211
224
  def create!(*args)
212
225
  props = args[0] || {}
213
- relationship_props = extract_relationship_attributes!(props)
226
+ association_props = extract_association_attributes!(props)
214
227
 
215
228
  new(*args).tap do |o|
216
229
  yield o if block_given?
217
230
  o.save!
218
- relationship_props.each do |prop, value|
231
+ association_props.each do |prop, value|
219
232
  o.send("#{prop}=", value)
220
233
  end
221
234
  end
@@ -229,10 +242,18 @@ module Neo4j::ActiveNode
229
242
 
230
243
  private
231
244
 
245
+ def create_magic_properties
246
+
247
+ end
248
+
232
249
  def update_magic_properties
233
250
  self.updated_at = DateTime.now if respond_to?(:updated_at=) && changed?
234
251
  end
235
252
 
253
+ def set_classname(props)
254
+ props[Neo4j::Config.class_name_property] = self.class.name if self.class.cached_class?
255
+ end
256
+
236
257
  def assign_attributes(attributes)
237
258
  attributes.each do |attribute, value|
238
259
  send("#{attribute}=", value)