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