neo4j 1.2.1-java → 1.2.2-java

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,10 +1,22 @@
1
- == 1.2.1 / 2011-08-29
1
+ == 1.2.2 / 2011-09-15
2
+ * Added compositions support for rails mode (Deepak N)
3
+ * Added support for nested transactions at the Rails model level (Vivek Prahlad)
4
+ * Fixing issue where save for invalid entities puts them into an inconsistent state (Vivek Prahlad)
5
+ * Fix for issue with save when validation fails (Vivek Prahlad)
6
+ * Fix for accepts_nested_attributes_for when the associated entities are created before a new node (Vivek Prahlad)
7
+ * Fix to allow has_one relationships to handle nil assignments in models (Vivek Prahlad)
8
+ * Observers support for neo4j rails model using active model (Deepak N)
9
+ * Override ActiveModel i18n_scope for neo4j (Deepak N)
10
+ * Added finders similar to active record and mongoid (Deepak N)
11
+ * Added find!, find_or_create_by and find_or_initialize_by methods, similar to active record finders (Deepak N)
12
+
13
+ == 1.2.1 / 2011-08-29
2
14
  * Fixed failing RSpecs for devise-neo4j gem - column_names method on neo4j orm adapter throws NoMethodError (thanks Deepak N)
3
15
 
4
- == 1.2.0 / 2011-08-16
16
+ == 1.2.0 / 2011-08-16
5
17
  * Upgrade to java library neo4j 1.4.1, see http://neo4j.rubyforge.org/guides/configuration.html
6
18
 
7
- == 1.1.4 / 2011-08-10
19
+ == 1.1.4 / 2011-08-10
8
20
  * Fixed dependency to will_paginate, locked to 3.0.pre4 (newly released 3.0.0 does not work yet with neo4j.rb)
9
21
 
10
22
  == 1.1.3 / 2011-08-09
@@ -12,12 +24,12 @@
12
24
  * BUG: not able to create array properties on relationships (Pere Urbon)
13
25
  * BUG: lucene did not work if starting up neo4j in read only mode (like rails console when the rails is already running)
14
26
 
15
- == 1.1.2 / 2011-06-08
27
+ == 1.1.2 / 2011-06-08
16
28
  * Added configuration option 'enable_rules' to disable the _all relationships and custom rules [#176]
17
29
  * Added a #node method on the Neo4j::Node and Neo4j::NodeMixin. Works like the #rel method but returns the node instead. [#174]
18
30
  * Simplify creating relationship between two existing nodes [#175]
19
31
 
20
- == 1.1.1 / 2011-05-26
32
+ == 1.1.1 / 2011-05-26
21
33
  * Made neo4j compatible with rails 3.1.0.rc1 [#170]
22
34
  * Fix for neo4j-devise [#171]
23
35
  * BUG: Neo4j::GraphAlgo shortest path does raise exception if two nodes are not connected [#172]
@@ -45,7 +57,7 @@
45
57
  * Lots of improvements of the API
46
58
  * Better ActiveModel/Rails integration
47
59
 
48
- == 0.4.4 / 2010-08-01
60
+ == 0.4.4 / 2010-08-01
49
61
  * Fixed bug on traversing when using the RelationshipMixin (#121)
50
62
  * BatchInserter and JRuby 1.6 - Fix iteration error with trying to modify in-place hash
51
63
 
data/CONTRIBUTORS CHANGED
@@ -3,6 +3,7 @@ Maintainer:
3
3
 
4
4
  Contributors:
5
5
 
6
+ * Vivek Prahlad
6
7
  * Deepak N
7
8
  * Frédéric Vanclef
8
9
  * Pere Urbon
data/Gemfile CHANGED
@@ -7,7 +7,10 @@ group 'test' do
7
7
  gem "rdoc", ">= 2.5.10"
8
8
  gem "horo", ">= 1.0.2"
9
9
  gem "rspec", ">= 2.0.0"
10
- gem "rspec-rails-matchers", ">= 0.2.1"
10
+
11
+ # use this version for rspec-rails-matchers which work with latest RSpec (Rspec => RSpec)
12
+ gem "rspec-rails-matchers", :git => 'git://github.com/afcapel/rspec-rails-matchers.git'
13
+
11
14
  gem "test-unit"
12
15
  gem 'rcov'
13
16
  end
data/lib/neo4j/node.rb CHANGED
@@ -49,17 +49,25 @@ module Neo4j
49
49
  # The property operations give access to the key-value property pairs.
50
50
  # Property keys are always strings. Valid property value types are the primitives(<tt>String</tt>, <tt>Fixnum</tt>, <tt>Float</tt>, <tt>Boolean</tt>), and arrays of those primitives.
51
51
  #
52
- # === Instance Methods form Included Mixins
52
+ # === Instance Methods from Included Mixins
53
53
  # * Neo4j::Property - methods that deal with properties
54
- # * Neo4j::NodeRelationship methods for relationship
54
+ # * Neo4j::Rels methods for accessing incoming and outgoing relationship and nodes of depth one.
55
55
  # * Neo4j::Equal equality operators: <tt>eql?</tt>, <tt>equal</tt>, <tt>==</tt>
56
56
  # * Neo4j::Index lucene index methods, like indexing a node
57
+ # * Neo4j::Traversal - provides an API for accessing outgoing and incoming nodes by traversing from this node of any depth.
57
58
  #
58
59
  # === Class Methods from Included Mixins
59
60
  # * Neo4j::Index::ClassMethods lucene index class methods, like find
60
61
  # * Neo4j::Load - methods for loading a node
61
62
  #
62
- # See also the Neo4j::NodeMixin (Neo4j::Mapping::NodeMixin) if you want to wrap a node with your own Ruby class.
63
+ # === Neo4j::Node#new and Wrappers
64
+ #
65
+ # The Neo4j::Node#new method does not return a new Ruby instance (!). Instead it will call the Neo4j Java API which will return a
66
+ # *org.neo4j.kernel.impl.core.NodeProxy* object. This java object includes those mixins, see above. The #class method on the java object
67
+ # returns Neo4j::Node in order to make it feel like an ordnary Ruby object.
68
+ #
69
+ # If you want to map your own class to a neo4j node you can use the Neo4j::NodeMixin or the Neo4j::Rails::Model.
70
+ # The Neo4j::NodeMixin and Neo4j::Rails::Model wraps the Neo4j::Node object. The raw java node/Neo4j::Node object can be access with the Neo4j::NodeMixin#java_node method.
63
71
  #
64
72
  class Node
65
73
  extend Neo4j::Index::ClassMethods
@@ -2,7 +2,15 @@ module Neo4j
2
2
  module Rails
3
3
  module Callbacks #:nodoc:
4
4
  extend ActiveSupport::Concern
5
-
5
+
6
+ CALLBACKS = [
7
+ :before_validation, :after_validation,
8
+ :before_create, :around_create, :after_create,
9
+ :before_destroy, :around_destroy, :after_destroy,
10
+ :before_save, :around_save, :after_save,
11
+ :before_update, :around_update, :after_update,
12
+ ].freeze
13
+
6
14
  included do
7
15
  [:valid?, :create_or_update, :create, :update, :destroy].each do |method|
8
16
  alias_method_chain method, :callbacks
@@ -0,0 +1,256 @@
1
+ module Neo4j
2
+ module Rails
3
+ # = Neo4j Rails Model Compositions
4
+ module Compositions # :nodoc:
5
+ extend ActiveSupport::Concern
6
+
7
+ def clear_composition_cache #:nodoc:
8
+ composition_cache.clear if persisted?
9
+ end
10
+
11
+ def composition_cache
12
+ @composition_cache ||= {}
13
+ end
14
+
15
+ # Neo4j Rails Model implements composition through a macro-like class method called +composed_of+
16
+ # for representing attributes as value objects. It expresses relationships like "Account [is]
17
+ # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call
18
+ # to the macro adds a description of how the value objects are created from the attributes of
19
+ # the entity object (when the entity is initialized either as a new object or from finding an
20
+ # existing object) and how it can be turned back into attributes (when the entity is saved to
21
+ # the database).
22
+ #
23
+ # class Customer < Neo4j::Rails::Model
24
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
25
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
26
+ # end
27
+ #
28
+ # The customer class now has the following methods to manipulate the value objects:
29
+ # * <tt>Customer#balance, Customer#balance=(money)</tt>
30
+ # * <tt>Customer#address, Customer#address=(address)</tt>
31
+ #
32
+ # These methods will operate with value objects like the ones described below:
33
+ #
34
+ # class Money
35
+ # include Comparable
36
+ # attr_reader :amount, :currency
37
+ # EXCHANGE_RATES = { "USD_TO_DKK" => 6 }
38
+ #
39
+ # def initialize(amount, currency = "USD")
40
+ # @amount, @currency = amount, currency
41
+ # end
42
+ #
43
+ # def exchange_to(other_currency)
44
+ # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
45
+ # Money.new(exchanged_amount, other_currency)
46
+ # end
47
+ #
48
+ # def ==(other_money)
49
+ # amount == other_money.amount && currency == other_money.currency
50
+ # end
51
+ #
52
+ # def <=>(other_money)
53
+ # if currency == other_money.currency
54
+ # amount <=> amount
55
+ # else
56
+ # amount <=> other_money.exchange_to(currency).amount
57
+ # end
58
+ # end
59
+ # end
60
+ #
61
+ # class Address
62
+ # attr_reader :street, :city
63
+ # def initialize(street, city)
64
+ # @street, @city = street, city
65
+ # end
66
+ #
67
+ # def close_to?(other_address)
68
+ # city == other_address.city
69
+ # end
70
+ #
71
+ # def ==(other_address)
72
+ # city == other_address.city && street == other_address.street
73
+ # end
74
+ # end
75
+ #
76
+ # Now it's possible to access attributes from the database through the value objects instead. If
77
+ # you choose to name the composition the same as the attribute's name, it will be the only way to
78
+ # access that attribute. That's the case with our +balance+ attribute. You interact with the value
79
+ # objects just like you would any other attribute, though:
80
+ #
81
+ # customer.balance = Money.new(20) # sets the Money value object and the attribute
82
+ # customer.balance # => Money value object
83
+ # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK")
84
+ # customer.balance > Money.new(10) # => true
85
+ # customer.balance == Money.new(20) # => true
86
+ # customer.balance < Money.new(5) # => false
87
+ #
88
+ # Value objects can also be composed of multiple attributes, such as the case of Address. The order
89
+ # of the mappings will determine the order of the parameters.
90
+ #
91
+ # customer.address_street = "Hyancintvej"
92
+ # customer.address_city = "Copenhagen"
93
+ # customer.address # => Address.new("Hyancintvej", "Copenhagen")
94
+ # customer.address = Address.new("May Street", "Chicago")
95
+ # customer.address_street # => "May Street"
96
+ # customer.address_city # => "Chicago"
97
+ #
98
+ # == Writing value objects
99
+ #
100
+ # Value objects are immutable and interchangeable objects that represent a given value, such as
101
+ # a Money object representing $5. Two Money objects both representing $5 should be equal (through
102
+ # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is
103
+ # unlike entity objects where equality is determined by identity. An entity class such as Customer can
104
+ # easily have two different objects that both have an address on Hyancintvej. Entity identity is
105
+ # determined by object or relational unique identifiers (such as primary keys). Normal
106
+ # Neo4j::Rails::Model classes are entity objects.
107
+ #
108
+ # It's also important to treat the value objects as immutable. Don't allow the Money object to have
109
+ # its amount changed after creation. Create a new Money object with the new value instead. This
110
+ # is exemplified by the Money#exchange_to method that returns a new value object instead of changing
111
+ # its own values. Neo4j Rails Model won't persist value objects that have been changed through means
112
+ # other than the writer method.
113
+ #
114
+ # The immutable requirement is enforced by Neo4j Rails Model by freezing any object assigned as a value
115
+ # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError.
116
+ #
117
+ # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
118
+ # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
119
+ #
120
+ # == Custom constructors and converters
121
+ #
122
+ # By default value objects are initialized by calling the <tt>new</tt> constructor of the value
123
+ # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt>
124
+ # option, as arguments. If the value class doesn't support this convention then +composed_of+ allows
125
+ # a custom constructor to be specified.
126
+ #
127
+ # When a new value is assigned to the value object the default assumption is that the new value
128
+ # is an instance of the value class. Specifying a custom converter allows the new value to be automatically
129
+ # converted to an instance of value class if necessary.
130
+ #
131
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
132
+ # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
133
+ # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
134
+ # values can be assigned to the value object using either another NetAddr::CIDR object, a string
135
+ # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
136
+ # these requirements:
137
+ #
138
+ # class NetworkResource < Neo4j::Rails::Model
139
+ # composed_of :cidr,
140
+ # :class_name => 'NetAddr::CIDR',
141
+ # :mapping => [ %w(network_address network), %w(cidr_range bits) ],
142
+ # :allow_nil => true,
143
+ # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
144
+ # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
145
+ # end
146
+ #
147
+ # # This calls the :constructor
148
+ # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
149
+ #
150
+ # # These assignments will both use the :converter
151
+ # network_resource.cidr = [ '192.168.2.1', 8 ]
152
+ # network_resource.cidr = '192.168.0.1/24'
153
+ #
154
+ # # This assignment won't use the :converter as the value is already an instance of the value class
155
+ # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')
156
+ #
157
+ # # Saving and then reloading will use the :constructor on reload
158
+ # network_resource.save
159
+ # network_resource.reload
160
+ #
161
+ # == [TBD] Finding records by a value object [TBD]
162
+ #
163
+ # Once a +composed_of+ relationship is specified for a model, records can be loaded from the database
164
+ # by specifying an instance of the value object in the conditions hash. The following example
165
+ # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
166
+ #
167
+ # Customer.where(:balance => Money.new(20, "USD")).all
168
+ #
169
+ module ClassMethods
170
+ # Adds reader and writer methods for manipulating a value object:
171
+ # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
172
+ #
173
+ # Options are:
174
+ # * <tt>:class_name</tt> - Specifies the class name of the association. Use it only if that name
175
+ # can't be inferred from the part id. So <tt>composed_of :address</tt> will by default be linked
176
+ # to the Address class, but if the real class name is CompanyAddress, you'll have to specify it
177
+ # with this option.
178
+ # * <tt>:mapping</tt> - Specifies the mapping of entity attributes to attributes of the value
179
+ # object. Each mapping is represented as an array where the first item is the name of the
180
+ # entity attribute and the second item is the name the attribute in the value object. The
181
+ # order in which mappings are defined determine the order in which attributes are sent to the
182
+ # value class constructor.
183
+ # * <tt>:allow_nil</tt> - Specifies that the value object will not be instantiated when all mapped
184
+ # attributes are +nil+. Setting the value object to +nil+ has the effect of writing +nil+ to all
185
+ # mapped attributes.
186
+ # This defaults to +false+.
187
+ # * <tt>:constructor</tt> - A symbol specifying the name of the constructor method or a Proc that
188
+ # is called to initialize the value object. The constructor is passed all of the mapped attributes,
189
+ # in the order that they are defined in the <tt>:mapping option</tt>, as arguments and uses them
190
+ # to instantiate a <tt>:class_name</tt> object.
191
+ # The default is <tt>:new</tt>.
192
+ # * <tt>:converter</tt> - A symbol specifying the name of a class method of <tt>:class_name</tt>
193
+ # or a Proc that is called when a new value is assigned to the value object. The converter is
194
+ # passed the single value that is used in the assignment and is only called if the new value is
195
+ # not an instance of <tt>:class_name</tt>.
196
+ #
197
+ # Option examples:
198
+ # composed_of :temperature, :mapping => %w(reading celsius)
199
+ # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
200
+ # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
201
+ # composed_of :gps_location
202
+ # composed_of :gps_location, :allow_nil => true
203
+ # composed_of :ip_address,
204
+ # :class_name => 'IPAddr',
205
+ # :mapping => %w(ip to_i),
206
+ # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
207
+ # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
208
+ #
209
+ def composed_of(part_id, options = {})
210
+ options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
211
+
212
+ name = part_id.id2name
213
+ class_name = options[:class_name] || name.camelize
214
+ mapping = options[:mapping] || [ name, name ]
215
+ mapping = [ mapping ] unless mapping.first.is_a?(Array)
216
+ allow_nil = options[:allow_nil] || false
217
+ constructor = options[:constructor] || :new
218
+ converter = options[:converter]
219
+
220
+ reader_method(name, class_name, mapping, allow_nil, constructor)
221
+ writer_method(name, class_name, mapping, allow_nil, converter)
222
+ end
223
+
224
+ private
225
+ def reader_method(name, class_name, mapping, allow_nil, constructor)
226
+ define_method(name) do
227
+ if composition_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !self[pair.first].nil? })
228
+ attrs = mapping.collect { |pair| self[pair.first] }
229
+ object = constructor.respond_to?(:call) ? constructor.call(*attrs) : class_name.constantize.send(constructor, *attrs)
230
+ composition_cache[name] = object
231
+ end
232
+ composition_cache[name]
233
+ end
234
+ end
235
+
236
+ def writer_method(name, class_name, mapping, allow_nil, converter)
237
+ define_method("#{name}=") do |part|
238
+ if part.nil? && allow_nil
239
+ mapping.each { |pair| self[pair.first] = nil }
240
+ composition_cache[name] = nil
241
+ else
242
+ unless part.is_a?(class_name.constantize) || converter.nil?
243
+ part = converter.respond_to?(:call) ?
244
+ converter.call(part) :
245
+ class_name.constantize.send(converter, part)
246
+ end
247
+
248
+ mapping.each { |pair| self[pair.first] = part.send(pair.last) }
249
+ composition_cache[name] = part.freeze
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
@@ -1,5 +1,8 @@
1
1
  module Neo4j
2
2
  module Rails
3
+ class RecordNotFoundError < StandardError
4
+ end
5
+
3
6
  module Finders
4
7
  extend ActiveSupport::Concern
5
8
 
@@ -81,6 +84,46 @@ module Neo4j
81
84
  end
82
85
  end
83
86
 
87
+ # Finds a model by given id or matching given criteria.
88
+ # When node not found, raises RecordNotFoundError
89
+ def find!(*args)
90
+ self.find(*args).tap do |result|
91
+ raise Neo4j::Rails::RecordNotFoundError if result.nil?
92
+ end
93
+ end
94
+
95
+ # Find the first Node given the conditions, or creates a new node
96
+ # with the conditions that were supplied.
97
+ #
98
+ # @example Find or create the node.
99
+ # Person.find_or_create_by(:name => "test")
100
+ #
101
+ # @param [ Hash ] attrs The attributes to check.
102
+ #
103
+ # @return [ Node ] A matching or newly created node.
104
+ def find_or_create_by(attrs = {}, &block)
105
+ find_or(:create, attrs, &block)
106
+ end
107
+
108
+ # Similar to find_or_create_by,calls create! instead of create
109
+ # Raises RecordInvalidError if model is invalid.
110
+ def find_or_create_by!(attrs = {}, &block)
111
+ find_or(:create!, attrs, &block)
112
+ end
113
+
114
+ # Find the first Node given the conditions, or initializes a new node
115
+ # with the conditions that were supplied.
116
+ #
117
+ # @example Find or initialize the node.
118
+ # Person.find_or_initialize_by(:name => "test")
119
+ #
120
+ # @param [ Hash ] attrs The attributes to check.
121
+ #
122
+ # @return [ Node ] A matching or newly initialized node.
123
+ def find_or_initialize_by(attrs = {}, &block)
124
+ find_or(:new, attrs, &block)
125
+ end
126
+
84
127
  def all(*args)
85
128
  if !conditions_in?(*args)
86
129
  # use the _all rule to recover all the stored instances of this node
@@ -160,6 +203,19 @@ module Neo4j
160
203
  Thread.current[:neo4j_lucene_connection] << hits
161
204
  hits
162
205
  end
206
+
207
+ # Find the first object or create/initialize it.
208
+ #
209
+ # @example Find or perform an action.
210
+ # Person.find_or(:create, :name => "Dev")
211
+ #
212
+ # @param [ Symbol ] method The method to invoke.
213
+ # @param [ Hash ] attrs The attributes to query or set.
214
+ #
215
+ # @return [ Node ] The first or new node.
216
+ def find_or(method, attrs = {}, &block)
217
+ first(:conditions => attrs) || send(method, attrs, &block)
218
+ end
163
219
  end
164
220
  end
165
221
  end
@@ -29,6 +29,7 @@ module Neo4j
29
29
  reset_attributes
30
30
  clear_relationships
31
31
  self.attributes = attributes if attributes.is_a?(Hash)
32
+ yield self if block_given?
32
33
  end
33
34
 
34
35
  def id
@@ -86,7 +87,7 @@ module Neo4j
86
87
  def entity_load(id)
87
88
  Neo4j::Node.load(id)
88
89
  end
89
-
90
+
90
91
  ##
91
92
  # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling
92
93
  # dates and times from the database. This is set to :local by default.
@@ -127,6 +128,13 @@ module Neo4j
127
128
 
128
129
  end
129
130
  end
131
+
132
+ # Set the i18n scope to overwrite ActiveModel.
133
+ #
134
+ # @return [ Symbol ] :neo4j
135
+ def i18n_scope
136
+ :neo4j
137
+ end
130
138
  end
131
139
  end
132
140
 
@@ -140,8 +148,10 @@ module Neo4j
140
148
  include Timestamps # handle created_at, updated_at timestamp properties
141
149
  include Validations # enable validations
142
150
  include Callbacks # enable callbacks
151
+ include ActiveModel::Observing # enable observers
143
152
  include Finders # ActiveRecord style find
144
153
  include Relationships # for none persisted relationships
154
+ include Compositions
145
155
  end
146
156
  end
147
157
  end
@@ -0,0 +1,161 @@
1
+ module Neo4j
2
+ module Rails
3
+ # Observer classes respond to life cycle callbacks to implement trigger-like
4
+ # behavior outside the original class. This is a great way to reduce the
5
+ # clutter that normally comes when the model class is burdened with
6
+ # functionality that doesn't pertain to the core responsibility of the
7
+ # class. Neo4j's observers work similar to ActiveRecord's. Example:
8
+ #
9
+ # class CommentObserver < Neo4j::Rails::Observer
10
+ # def after_save(comment)
11
+ # Notifications.comment(
12
+ # "admin@do.com", "New comment was posted", comment
13
+ # ).deliver
14
+ # end
15
+ # end
16
+ #
17
+ # This Observer sends an email when a Comment#save is finished.
18
+ #
19
+ # class ContactObserver < Neo4j::Rails::Observer
20
+ # def after_create(contact)
21
+ # contact.logger.info('New contact added!')
22
+ # end
23
+ #
24
+ # def after_destroy(contact)
25
+ # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
26
+ # end
27
+ # end
28
+ #
29
+ # This Observer uses logger to log when specific callbacks are triggered.
30
+ #
31
+ # == Observing a class that can't be inferred
32
+ #
33
+ # Observers will by default be mapped to the class with which they share a
34
+ # name. So CommentObserver will be tied to observing Comment,
35
+ # ProductManagerObserver to ProductManager, and so on. If you want to
36
+ # name your observer differently than the class you're interested in
37
+ # observing, you can use the Observer.observe class method which takes
38
+ # either the concrete class (Product) or a symbol for that class (:product):
39
+ #
40
+ # class AuditObserver < Neo4j::Rails::Observer
41
+ # observe :account
42
+ #
43
+ # def after_update(account)
44
+ # AuditTrail.new(account, "UPDATED")
45
+ # end
46
+ # end
47
+ #
48
+ # If the audit observer needs to watch more than one kind of object,
49
+ # this can be specified with multiple arguments:
50
+ #
51
+ # class AuditObserver < Neo4j::Rails::Observer
52
+ # observe :account, :balance
53
+ #
54
+ # def after_update(record)
55
+ # AuditTrail.new(record, "UPDATED")
56
+ # end
57
+ # end
58
+ #
59
+ # The AuditObserver will now act on both updates to Account and Balance
60
+ # by treating them both as records.
61
+ #
62
+ # == Available callback methods
63
+ #
64
+ # * before_validation
65
+ # * after_validation
66
+ # * before_create
67
+ # * around_create
68
+ # * after_create
69
+ # * before_update
70
+ # * around_update
71
+ # * after_update
72
+ # * before_save
73
+ # * around_save
74
+ # * after_save
75
+ # * before_destroy
76
+ # * around_destroy
77
+ # * after_destroy
78
+ #
79
+ # == Storing Observers in Rails
80
+ #
81
+ # If you're using Neo4j within Rails, observer classes are usually stored
82
+ # in +app/models+ with the naming convention of +app/models/audit_observer.rb+.
83
+ #
84
+ # == Configuration
85
+ #
86
+ # In order to activate an observer, list it in the +config.neo4j.observers+
87
+ # configuration setting in your +config/application.rb+ file.
88
+ #
89
+ # config.neo4j.observers = :comment_observer, :signup_observer
90
+ #
91
+ # Observers will not be invoked unless you define them in your
92
+ # application configuration.
93
+ #
94
+ # == Loading
95
+ #
96
+ # Observers register themselves with the model class that they observe,
97
+ # since it is the class that notifies them of events when they occur.
98
+ # As a side-effect, when an observer is loaded, its corresponding model
99
+ # class is loaded.
100
+ #
101
+ # Observers are loaded after the application initializers, so that
102
+ # observed models can make use of extensions. If by any chance you are
103
+ # using observed models in the initialization, you can
104
+ # still load their observers by calling +ModelObserver.instance+ before.
105
+ # Observers are singletons and that call instantiates and registers them.
106
+ class Observer < ActiveModel::Observer
107
+
108
+ # Instantiate the new observer. Will add all child observers as well.
109
+ #
110
+ # @example Instantiate the observer.
111
+ # Neo4j::Rails::Observer.new
112
+ def initialize
113
+ super and observed_descendants.each { |klass| add_observer!(klass) }
114
+ end
115
+
116
+ protected
117
+
118
+ # Get all the child observers.
119
+ #
120
+ # @example Get the children.
121
+ # observer.observed_descendants
122
+ #
123
+ # @return [ Array<Class> ] The children.
124
+ def observed_descendants
125
+ observed_classes.inject([]) { |all, klass| all += klass.descendants }
126
+ end
127
+
128
+ # Adds the specified observer to the class.
129
+ #
130
+ # @example Add the observer.
131
+ # observer.add_observer!(Document)
132
+ #
133
+ # @param [ Class ] klass The child observer to add.
134
+ def add_observer!(klass)
135
+ super and define_callbacks(klass)
136
+ end
137
+
138
+ # Defines all the callbacks for each observer of the model.
139
+ #
140
+ # @example Define all the callbacks.
141
+ # observer.define_callbacks(Document)
142
+ #
143
+ # @param [ Class ] klass The model to define them on.
144
+ def define_callbacks(klass)
145
+ tap do |observer|
146
+ observer_name = observer.class.name.underscore.gsub('/', '__')
147
+ Neo4j::Rails::Callbacks::CALLBACKS.each do |callback|
148
+ next unless respond_to?(callback)
149
+ callback_meth = :"_notify_#{observer_name}_for_#{callback}"
150
+ unless klass.respond_to?(callback_meth)
151
+ klass.send(:define_method, callback_meth) do |&block|
152
+ observer.send(callback, self, &block)
153
+ end
154
+ klass.send(callback, callback_meth)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -73,6 +73,7 @@ module Neo4j
73
73
  def reload(options = nil)
74
74
  clear_changes
75
75
  clear_relationships
76
+ clear_composition_cache
76
77
  reset_attributes
77
78
  unless reload_from_database
78
79
  set_deleted_properties
@@ -196,10 +197,13 @@ module Neo4j
196
197
  write_attribute(attribute, value) if changed_attributes.has_key?(attribute)
197
198
  end
198
199
  end
199
-
200
- def _add_relationship(rel_type, attr)
200
+
201
+ def _create_entity(rel_type, attr)
201
202
  clazz = self.class._decl_rels[rel_type.to_sym].target_class
202
- node = clazz.new(attr)
203
+ _add_relationship(rel_type, clazz.new(attr))
204
+ end
205
+
206
+ def _add_relationship(rel_type, node)
203
207
  if respond_to?("#{rel_type}=")
204
208
  send("#{rel_type}=", node)
205
209
  elsif respond_to?("#{rel_type}")
@@ -220,32 +224,29 @@ module Neo4j
220
224
  raise "oops #{rel_type}"
221
225
  end
222
226
  end
227
+
228
+ def _has_relationship(rel_type, id)
229
+ !_find_node(rel_type,id).nil?
230
+ end
223
231
 
224
232
  def update_nested_attributes(rel_type, attr, options)
225
233
  allow_destroy, reject_if = [options[:allow_destroy], options[:reject_if]] if options
226
-
227
- if new?
228
- # We are updating a node that was created with the 'new' method.
229
- # The relationship will only be kept in the Value object.
230
- _add_relationship(rel_type, attr) unless reject_if?(reject_if, attr) || (allow_destroy && attr[:_destroy] && attr[:_destroy] != '0')
231
- else
232
- # We have a node that was created with the #create method - has real Neo4j relationships
233
- # does it exist ?
234
- found = _find_node(rel_type, attr[:id])
235
-
234
+ begin
236
235
  # Check if we want to destroy not found nodes (e.g. {..., :_destroy => '1' } ?
237
- destroy = attr[:_destroy] && attr[:_destroy] != '0'
238
-
239
- if found
240
- if destroy
241
- found.destroy if allow_destroy
236
+ destroy = allow_destroy && attr[:_destroy] && attr[:_destroy] != '0'
237
+ found = Neo4j::Node.load(attr[:id])
238
+ if destroy
239
+ found.destroy if found
240
+ else
241
+ if not found
242
+ _create_entity(rel_type, attr) #Create new node from scratch
242
243
  else
243
- found.update_attributes(attr) # it already exist, so update that one
244
+ #Create relationship to existing node in case it doesn't exist already
245
+ _add_relationship(rel_type, found) if (not _has_relationship(rel_type,attr[:id]))
246
+ found.update_attributes(attr)
244
247
  end
245
- elsif !destroy && !reject_if?(reject_if, attr)
246
- _add_relationship(rel_type, attr)
247
248
  end
248
- end
249
+ end unless reject_if?(reject_if, attr)
249
250
  end
250
251
 
251
252
  public
@@ -9,6 +9,8 @@ require 'neo4j/rails/finders'
9
9
  require 'neo4j/rails/mapping/property'
10
10
  require 'neo4j/rails/validations'
11
11
  require 'neo4j/rails/callbacks'
12
+ require 'neo4j/rails/observer'
13
+ require 'neo4j/rails/compositions'
12
14
  require 'neo4j/rails/timestamps'
13
15
  require 'neo4j/rails/serialization'
14
16
  require 'neo4j/rails/attributes'
@@ -5,7 +5,7 @@ module Neo4j
5
5
  initializer "neo4j.tx" do |app|
6
6
  app.config.middleware.use Neo4j::Rails::LuceneConnectionCloser
7
7
  end
8
-
8
+
9
9
  # Add ActiveModel translations to the I18n load_path
10
10
  initializer "i18n" do |app|
11
11
  config.i18n.load_path += Dir[File.join(File.dirname(__FILE__), '..', '..', '..', 'config', 'locales', '*.{rb,yml}')]
@@ -16,5 +16,18 @@ module Neo4j
16
16
  initializer "neo4j.start", :after => :load_config_initializers do |app|
17
17
  Neo4j::Config.setup.merge!(app.config.neo4j.to_hash)
18
18
  end
19
+
20
+ # Instantitate any registered observers after Rails initialization and
21
+ # instantiate them after being reloaded in the development environment
22
+ initializer "instantiate.observers" do
23
+ config.after_initialize do
24
+ ::Neo4j::Rails::Model.observers = config.neo4j.observers || []
25
+ ::Neo4j::Rails::Model.instantiate_observers
26
+
27
+ ActionDispatch::Callbacks.to_prepare do
28
+ ::Neo4j::Rails::Model.instantiate_observers
29
+ end
30
+ end
31
+ end
19
32
  end
20
33
  end
@@ -149,16 +149,18 @@ module Neo4j
149
149
  end
150
150
 
151
151
  def create()
152
- # prevent calling create twice
153
- @start_node.rm_outgoing_rel(type, self)
154
- @end_node.rm_incoming_rel(type, self)
155
-
156
- _persist_start_node
157
- _persist_end_node
158
-
159
- @_java_rel = Neo4j::Relationship.new(type, start_node, end_node)
160
- init_on_create
161
- clear_changes
152
+ begin
153
+ # prevent calling create twice
154
+ @start_node.rm_outgoing_rel(type, self)
155
+ @end_node.rm_incoming_rel(type, self)
156
+
157
+ _persist_start_node
158
+ _persist_end_node
159
+
160
+ @_java_rel = Neo4j::Relationship.new(type, start_node, end_node)
161
+ init_on_create
162
+ clear_changes
163
+ end unless @end_node.nil?
162
164
  true
163
165
  end
164
166
 
@@ -141,7 +141,7 @@ module Neo4j
141
141
  @incoming_rels.clear
142
142
 
143
143
  out_rels.each do |rel|
144
- rel.end_node.rm_incoming_rel(@rel_type.to_sym, rel)
144
+ rel.end_node.rm_incoming_rel(@rel_type.to_sym, rel) if rel.end_node
145
145
  success = rel.persisted? || rel.save
146
146
  # don't think this can happen - just in case, TODO
147
147
  raise "Can't save outgoing #{rel}, validation errors ? #{rel.errors.inspect}" unless success
@@ -12,11 +12,6 @@ module Neo4j
12
12
  # end
13
13
  class Transaction
14
14
  class << self
15
- def new
16
- finish if Thread.current[:neo4j_transaction]
17
- Thread.current[:neo4j_transaction] = Neo4j::Transaction.new
18
- end
19
-
20
15
  def current
21
16
  Thread.current[:neo4j_transaction]
22
17
  end
@@ -50,16 +45,25 @@ module Neo4j
50
45
  end
51
46
 
52
47
  def run
53
- begin
54
- new
55
- ret = yield self
56
- rescue
57
- fail
58
- raise
59
- ensure
60
- finish
48
+ if running?
49
+ yield self
50
+ else
51
+ begin
52
+ new
53
+ ret = yield self
54
+ rescue
55
+ fail
56
+ raise
57
+ ensure
58
+ finish
59
+ end
60
+ ret
61
61
  end
62
- ret
62
+ end
63
+
64
+ private
65
+ def new
66
+ Thread.current[:neo4j_transaction] = Neo4j::Transaction.new
63
67
  end
64
68
  end
65
69
  end
@@ -6,7 +6,7 @@ module Neo4j
6
6
  tx_method = "#{method}_in_tx"
7
7
  send(:alias_method, tx_method, method)
8
8
  send(:define_method, method) do |*args|
9
- Neo4j::Rails::Transaction.running? ? send(tx_method, *args) : Neo4j::Rails::Transaction.run { send(tx_method, *args) }
9
+ Neo4j::Rails::Transaction.run { send(tx_method, *args) }
10
10
  end
11
11
  end
12
12
  end
@@ -10,7 +10,11 @@ module Neo4j
10
10
  # The validation process on save can be skipped by passing false. The regular Model#save method is
11
11
  # replaced with this when the validations module is mixed in, which it is by default.
12
12
  def save(options={})
13
- perform_validations(options) ? super : false
13
+ result = perform_validations(options) ? super : false
14
+ if !result
15
+ Neo4j::Rails::Transaction.fail if Neo4j::Rails::Transaction.running?
16
+ end
17
+ result
14
18
  end
15
19
 
16
20
  def valid?(context = nil)
data/lib/neo4j/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Neo4j
2
- VERSION = "1.2.1"
2
+ VERSION = "1.2.2"
3
3
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: neo4j
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 1.2.1
5
+ version: 1.2.2
6
6
  platform: java
7
7
  authors:
8
8
  - Andreas Ronge
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-08-29 00:00:00 +02:00
13
+ date: 2011-09-15 00:00:00 +02:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -128,6 +128,7 @@ files:
128
128
  - lib/neo4j/rails/railtie.rb
129
129
  - lib/neo4j/rails/rails.rb
130
130
  - lib/neo4j/rails/lucene_connection_closer.rb
131
+ - lib/neo4j/rails/observer.rb
131
132
  - lib/neo4j/rails/transaction.rb
132
133
  - lib/neo4j/rails/timestamps.rb
133
134
  - lib/neo4j/rails/callbacks.rb
@@ -135,6 +136,7 @@ files:
135
136
  - lib/neo4j/rails/rel_persistence.rb
136
137
  - lib/neo4j/rails/finders.rb
137
138
  - lib/neo4j/rails/relationship.rb
139
+ - lib/neo4j/rails/compositions.rb
138
140
  - lib/neo4j/rails/relationships/rels_dsl.rb
139
141
  - lib/neo4j/rails/relationships/node_dsl.rb
140
142
  - lib/neo4j/rails/relationships/relationships.rb