neo4j 1.2.1-java → 1.2.2-java

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