From 8f07abfb45679bebb4126f67d34c33f855e19a87 Mon Sep 17 00:00:00 2001 From: Heinrich Klobuczek Date: Tue, 20 Sep 2016 21:38:02 -0700 Subject: [PATCH 1/4] undeclared properties --- lib/neo4j/shared/mass_assignment.rb | 6 +++++- lib/neo4j/shared/persistence.rb | 12 ++++++++++-- lib/neo4j/shared/property.rb | 5 +++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/neo4j/shared/mass_assignment.rb b/lib/neo4j/shared/mass_assignment.rb index 552c9b9bd..c9d47edbd 100644 --- a/lib/neo4j/shared/mass_assignment.rb +++ b/lib/neo4j/shared/mass_assignment.rb @@ -26,7 +26,11 @@ def assign_attributes(new_attributes = nil) return unless new_attributes.present? new_attributes.each do |name, value| writer = :"#{name}=" - send(writer, value) if respond_to?(writer) + if respond_to?(writer) + send(writer, value) + else + (@undeclared_attributes ||= {})[name] = value + end end end diff --git a/lib/neo4j/shared/persistence.rb b/lib/neo4j/shared/persistence.rb index ac16e7437..a0ef5a633 100644 --- a/lib/neo4j/shared/persistence.rb +++ b/lib/neo4j/shared/persistence.rb @@ -8,7 +8,7 @@ def props_for_persistence end def update_model - return if !changed_attributes || changed_attributes.empty? + return if (!changed_attributes || changed_attributes.empty?) && @undeclared_attributes.blank? neo4j_query(query_as(:n).set(n: props_for_update)) changed_attributes.clear end @@ -24,6 +24,7 @@ def props_for_create inject_timestamps! props_with_defaults = inject_defaults!(props) converted_props = props_for_db(props_with_defaults) + inject_undeclared_attributes!(converted_props) return converted_props unless self.class.respond_to?(:default_property_values) inject_primary_key!(converted_props) end @@ -34,7 +35,14 @@ def props_for_update changed_props = attributes.select { |k, _| changed_attributes.include?(k) } changed_props.symbolize_keys! inject_defaults!(changed_props) - props_for_db(changed_props) + changed_props = props_for_db(changed_props) + inject_undeclared_attributes!(changed_props) + changed_props + end + + def inject_undeclared_attributes!(converted_props) + converted_props.merge!(@undeclared_attributes) if @undeclared_attributes.present? + @undeclared_attributes = nil end # Increments a numeric attribute by a centain amount diff --git a/lib/neo4j/shared/property.rb b/lib/neo4j/shared/property.rb index ded0b13dd..41c1289c8 100644 --- a/lib/neo4j/shared/property.rb +++ b/lib/neo4j/shared/property.rb @@ -23,9 +23,10 @@ def inspect def initialize(attributes = nil) attributes = process_attributes(attributes) modded_attributes = inject_defaults!(attributes) - validate_attributes!(modded_attributes) + # validate_attributes!(modded_attributes) writer_method_props = extract_writer_methods!(modded_attributes) send_props(writer_method_props) + @undeclared_attributes = attributes @_persisted_obj = nil end @@ -35,7 +36,7 @@ def inject_defaults!(starting_props) end def read_attribute(name) - respond_to?(name) ? send(name) : nil + respond_to?(name) ? send(name) : (_persisted_obj && _persisted_obj.props[name]) end alias [] read_attribute From 502355f868a17e31462379235287ae6a558e0560 Mon Sep 17 00:00:00 2001 From: Heinrich Klobuczek Date: Thu, 22 Sep 2016 06:24:47 -0700 Subject: [PATCH 2/4] nearly all specs pass --- lib/neo4j.rb | 1 + lib/neo4j/shared/mass_assignment.rb | 4 +- lib/neo4j/shared/persistence.rb | 24 +++--- lib/neo4j/shared/property.rb | 8 +- lib/neo4j/undeclared_properties.rb | 53 +++++++++++++ spec/e2e/undeclared_properties_spec.rb | 100 +++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 lib/neo4j/undeclared_properties.rb create mode 100644 spec/e2e/undeclared_properties_spec.rb diff --git a/lib/neo4j.rb b/lib/neo4j.rb index 3176307cb..3f378af88 100644 --- a/lib/neo4j.rb +++ b/lib/neo4j.rb @@ -26,6 +26,7 @@ require 'neo4j/schema/operation' require 'neo4j/timestamps' +require 'neo4j/undeclared_properties' require 'neo4j/shared/callbacks' require 'neo4j/shared/filtered_hash' diff --git a/lib/neo4j/shared/mass_assignment.rb b/lib/neo4j/shared/mass_assignment.rb index c9d47edbd..008fe4393 100644 --- a/lib/neo4j/shared/mass_assignment.rb +++ b/lib/neo4j/shared/mass_assignment.rb @@ -29,11 +29,13 @@ def assign_attributes(new_attributes = nil) if respond_to?(writer) send(writer, value) else - (@undeclared_attributes ||= {})[name] = value + add_undeclared_property(name, value) end end end + def add_undeclared_property(_, _); end + # Mass update a model's attributes # # @example Assigning a hash diff --git a/lib/neo4j/shared/persistence.rb b/lib/neo4j/shared/persistence.rb index a0ef5a633..c4626991c 100644 --- a/lib/neo4j/shared/persistence.rb +++ b/lib/neo4j/shared/persistence.rb @@ -8,11 +8,17 @@ def props_for_persistence end def update_model - return if (!changed_attributes || changed_attributes.empty?) && @undeclared_attributes.blank? - neo4j_query(query_as(:n).set(n: props_for_update)) + return if skip_update? + props = props_for_update + neo4j_query(query_as(:n).set(n: props)) + _persisted_obj.props.merge!(props) changed_attributes.clear end + def skip_update? + changed_attributes.blank? + end + # Returns a hash containing: # * All properties and values for insertion in the database # * A `uuid` (or equivalent) key and value @@ -24,7 +30,6 @@ def props_for_create inject_timestamps! props_with_defaults = inject_defaults!(props) converted_props = props_for_db(props_with_defaults) - inject_undeclared_attributes!(converted_props) return converted_props unless self.class.respond_to?(:default_property_values) inject_primary_key!(converted_props) end @@ -35,14 +40,7 @@ def props_for_update changed_props = attributes.select { |k, _| changed_attributes.include?(k) } changed_props.symbolize_keys! inject_defaults!(changed_props) - changed_props = props_for_db(changed_props) - inject_undeclared_attributes!(changed_props) - changed_props - end - - def inject_undeclared_attributes!(converted_props) - converted_props.merge!(@undeclared_attributes) if @undeclared_attributes.present? - @undeclared_attributes = nil + props_for_db(changed_props) end # Increments a numeric attribute by a centain amount @@ -72,7 +70,7 @@ def concurrent_increment!(_attribute, _by = 1) # @param [Symbol, String] attribute of the attribute to update # @param [Object] value to set def update_attribute(attribute, value) - send("#{attribute}=", value) + write_attribute(attribute, value) self.save end @@ -80,7 +78,7 @@ def update_attribute(attribute, value) # @param [Symbol, String] attribute of the attribute to update # @param [Object] value to set def update_attribute!(attribute, value) - send("#{attribute}=", value) + write_attribute(attribute, value) self.save! end diff --git a/lib/neo4j/shared/property.rb b/lib/neo4j/shared/property.rb index 41c1289c8..c708c1797 100644 --- a/lib/neo4j/shared/property.rb +++ b/lib/neo4j/shared/property.rb @@ -23,20 +23,22 @@ def inspect def initialize(attributes = nil) attributes = process_attributes(attributes) modded_attributes = inject_defaults!(attributes) - # validate_attributes!(modded_attributes) + validate_attributes!(modded_attributes) writer_method_props = extract_writer_methods!(modded_attributes) send_props(writer_method_props) - @undeclared_attributes = attributes + self.undeclared_properties = attributes @_persisted_obj = nil end + def undeclared_properties=(_); end + def inject_defaults!(starting_props) return starting_props if self.class.declared_properties.declared_property_defaults.empty? self.class.declared_properties.inject_defaults!(self, starting_props || {}) end def read_attribute(name) - respond_to?(name) ? send(name) : (_persisted_obj && _persisted_obj.props[name]) + respond_to?(name) ? send(name) : nil end alias [] read_attribute diff --git a/lib/neo4j/undeclared_properties.rb b/lib/neo4j/undeclared_properties.rb new file mode 100644 index 000000000..1c0544d44 --- /dev/null +++ b/lib/neo4j/undeclared_properties.rb @@ -0,0 +1,53 @@ +module Neo4j + # This mixin allows storage and update of undeclared properties in the included class + module UndeclaredProperties + extend ActiveSupport::Concern + + included do + attr_accessor :undeclared_properties + end + + def validate_attributes!(_) + end + + def read_attribute(name) + respond_to?(name) ? super(name) : read_undeclared_property(name) + end + alias [] read_attribute + + def read_undeclared_property(name) + _persisted_obj ? _persisted_obj.props[name] : (undeclared_properties && undeclared_properties[name]) + end + + def write_attribute(name, value) + if respond_to? "#{name}=" + super(name, value) + else + add_undeclared_property(name, value) + end + end + alias []= write_attribute + + def skip_update? + super && undeclared_properties.blank? + end + + def props_for_create + super.merge(undeclared_properties!) + end + + def props_for_update + super.merge(undeclared_properties!) + end + + def undeclared_properties! + undeclared_properties || {} + ensure + self.undeclared_properties = nil + end + + def add_undeclared_property(name, value) + (self.undeclared_properties ||= {})[name] = value + end + end +end diff --git a/spec/e2e/undeclared_properties_spec.rb b/spec/e2e/undeclared_properties_spec.rb new file mode 100644 index 000000000..b3707d7f6 --- /dev/null +++ b/spec/e2e/undeclared_properties_spec.rb @@ -0,0 +1,100 @@ +describe Neo4j::ActiveNode do + before(:each) do + stub_active_node_class('Person') do + include Neo4j::UndeclaredProperties + property :name, type: String + end + end + + def get_value_from_db(node, prop) + node.class.where(id: node.id).first[prop] + end + + describe '.new' do + it 'undeclared properties are found' do + expect(Person.new(foo: 123)[:foo]).to eq(123) + expect(Person.new(foo: 123)['foo']).to eq(123) + end + end + + describe '.create' do + it 'does allow to set undeclared properties using create' do + expect { Person.create(foo: 43) }.not_to raise_error Neo4j::Shared::Property::UndefinedPropertyError + end + + it 'stores undefined attributes' do + person = Person.create(name: 'Jim', foo: 123) + expect(person[:foo]).to eq(123) + expect(person['foo']).to eq(123) + expect(get_value_from_db(person, :foo)).to eq(123) + end + end + + describe 'save' do + it 'does not raise Neo4j::Shared::UnknownAttributeError if trying to set undeclared property' do + expect { Person.new[:foo] = 42 }.not_to raise_error(Neo4j::UnknownAttributeError) + end + + it 'saves undeclared the properties that has been changed with []= operator' do + person = Person.new + person[:foo] = 123 + person.save + expect(person[:foo]).to eq(123) + expect(get_value_from_db(person, :foo)).to eq(123) + end + end + + describe '#save!' do + it 'returns true on success' do + person = Person.new + person[:name] = 'Jim' + person[:foo] = 123 + expect(person.save!).to be true + end + end + + describe 'update_attributes' do + let(:person) do + Person.create(name: 'Jim', foo: 123) + end + + it 'updates given declared and udeclared property' do + person.update_attributes(name: 'Joe', foo: 456) + expect(get_value_from_db(person, :name)).to eq('Joe') + expect(get_value_from_db(person, :foo)).to eq(456) + end + + it 'does delete undeclared property' do + person.update_attributes(foo: nil) + expect(get_value_from_db(person, :foo)).to be_nil + end + end + + describe 'update_attribute' do + let(:person) do + Person.create(name: 'Jim') + end + + it 'updates given udeclared property' do + person.update_attribute(:foo, 123) + expect(get_value_from_db(person, :foo)).to eq(123) + end + + it 'does delete undeclared property' do + person.update_attribute(:foo, 123) + person.update_attribute(:foo, nil) + expect(get_value_from_db(person, :foo)).to be_nil + end + end + + describe 'update_attribute!' do + let(:person) do + Person.create(name: 'Jim') + end + + it 'updates given property' do + person.update_attribute!(:foo, 123) + expect(get_value_from_db(person, :foo)).to eq(123) + end + end +end From 205ae5f206ae97268a438920b16e2aa55483bba0 Mon Sep 17 00:00:00 2001 From: Heinrich Klobuczek Date: Thu, 22 Sep 2016 07:12:24 -0700 Subject: [PATCH 3/4] documentation --- docs/Properties.rst | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/Properties.rst b/docs/Properties.rst index 9fbddba28..84d3041af 100644 --- a/docs/Properties.rst +++ b/docs/Properties.rst @@ -43,16 +43,26 @@ You will now be able to set the ``title`` property through mass-assignment (``Po Undeclared Properties --------------------- -Neo4j, being schemaless as far as the database is concerned, does not require that property keys be defined ahead of time. As a result, it's possible (and sometimes desirable) to set properties on the node that are not also defined on the database. For instance: +Neo4j, being schemaless as far as the database is concerned, does not require that property keys be defined ahead of time. As a result, it's possible (and sometimes desirable) to set properties on the node that are not also defined on the database. By including the module ``Neo4j::UndeclaredProperties`` no exceptions will be thrown if unknown attributes are passed to selected methods. + .. code-block:: ruby - Neo4j::Node.create({ property: 'MyProp', secret_val: 123 }, :Post) + class Post + include Neo4j::ActiveNode + include Neo4j::UndeclaredProperties + + property :title + end + + Post.create(title: 'My Post', secret_val: 123) post = Post.first - post.secret_val - => NoMethodError: undefined method `secret_val`... + post.secret_val #=> NoMethodError: undefined method `secret_val` + post[:secret_val] #=> 123... + -In this case, simply adding the ``secret_val`` property to your model will make it available through the ``secret_val`` method. Alternatively, you can also access the properties of the "unwrapped node" through ``post._persisted_obj.props``. See the Neo4j::Core API for more details about working with CypherNode objects. +In this case, simply adding the ``secret_val`` property to your model will make it available through the ``secret_val`` method. +The module supports undeclared properties in the following methods: `new`, `create`, `[]`, `[]=`, `update_attribute`, `update_attribute!`, `update_attributes` and their corresponding aliases. Types and Conversion ____________________ From c26a456633778d3cec70434d731855ace45b6e21 Mon Sep 17 00:00:00 2001 From: Heinrich Klobuczek Date: Thu, 22 Sep 2016 16:39:12 -0700 Subject: [PATCH 4/4] allowing strings as property keys --- lib/neo4j/undeclared_properties.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/neo4j/undeclared_properties.rb b/lib/neo4j/undeclared_properties.rb index 1c0544d44..1346aad69 100644 --- a/lib/neo4j/undeclared_properties.rb +++ b/lib/neo4j/undeclared_properties.rb @@ -11,7 +11,7 @@ def validate_attributes!(_) end def read_attribute(name) - respond_to?(name) ? super(name) : read_undeclared_property(name) + respond_to?(name) ? super(name) : read_undeclared_property(name.to_sym) end alias [] read_attribute