Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Undeclared properties #1294

Merged
merged 6 commits into from Sep 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 15 additions & 5 deletions docs/Properties.rst
Expand Up @@ -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
____________________
Expand Down
1 change: 1 addition & 0 deletions lib/neo4j.rb
Expand Up @@ -26,6 +26,7 @@
require 'neo4j/schema/operation'

require 'neo4j/timestamps'
require 'neo4j/undeclared_properties'

require 'neo4j/shared/callbacks'
require 'neo4j/shared/filtered_hash'
Expand Down
8 changes: 7 additions & 1 deletion lib/neo4j/shared/mass_assignment.rb
Expand Up @@ -26,10 +26,16 @@ 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
add_undeclared_property(name, value)
end
end
end

def add_undeclared_property(_, _); end

# Mass update a model's attributes
#
# @example Assigning a hash
Expand Down
14 changes: 10 additions & 4 deletions lib/neo4j/shared/persistence.rb
Expand Up @@ -10,11 +10,17 @@ def props_for_persistence
end

def update_model
return if !changed_attributes || changed_attributes.empty?
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
Expand Down Expand Up @@ -66,15 +72,15 @@ 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

# Convenience method to set attribute and #save! at the same time
# @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

Expand Down
3 changes: 3 additions & 0 deletions lib/neo4j/shared/property.rb
Expand Up @@ -26,9 +26,12 @@ def initialize(attributes = nil)
validate_attributes!(modded_attributes)
writer_method_props = extract_writer_methods!(modded_attributes)
send_props(writer_method_props)
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 || {})
Expand Down
53 changes: 53 additions & 0 deletions 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.to_sym)
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
100 changes: 100 additions & 0 deletions 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