diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 60e0b4e6..8dece9b7 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -218,9 +218,9 @@ def initialize(*args, **kwargs) end class Base < Document - include ::ActiveRecord::Validations include Persistence include ::ActiveRecord::AttributeMethods::Dirty + include ::ActiveRecord::Validations # must be included after Persistence include ::ActiveRecord::Timestamp # must be included after Persistence include Associations @@ -339,5 +339,11 @@ def eql?(other) def ==(other) super || other.instance_of?(self.class) && !id.nil? && other.id == id end + + private + + def raise_validation_error + raise CouchbaseOrm::Error::RecordInvalid.new(self) + end end end diff --git a/lib/couchbase-orm/error.rb b/lib/couchbase-orm/error.rb index 6aa7ebcc..aaab0e3e 100644 --- a/lib/couchbase-orm/error.rb +++ b/lib/couchbase-orm/error.rb @@ -10,20 +10,20 @@ def initialize(message = nil, record = nil) end class RecordInvalid < Error - def initialize(message = nil, record = nil) + def initialize(record = nil) if record - errors = record.errors.full_messages.join(", ") - message = I18n.t( - :"couchbase.#{record.class.design_document}.errors.messages.record_invalid", - errors: errors, - default: :"couchbase.errors.messages.record_invalid" - ) + @record = record + errors = @record.errors.full_messages.join(", ") + message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid") + else + message = "Record invalid" end super(message, record) end end class TypeMismatchError < Error; end class RecordExists < Error; end - class CouchbaseOrm::Error::EmptyNotAllowed < Error; end + class EmptyNotAllowed < Error; end + class DocumentNotFound < Error; end end end diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index efa6f8ec..a837e404 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -79,9 +79,10 @@ def persisted? # # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. - def save(**options) + def save(**options, &block) raise "Cannot save a destroyed document!" if destroyed? - self.new_record? ? _create_record(**options) : _update_record(**options) + @_with_cas = options[:with_cas] + create_or_update(**options, &block) end # Saves the model. @@ -91,7 +92,7 @@ def save(**options) # # By default, #save! always runs validations. If any of them fail # CouchbaseOrm::Error::RecordInvalid gets raised, and the record won't be saved. - def save!(**options) + def save!(**options, &block) self.class.fail_validate!(self) unless self.save(**options) self end @@ -101,8 +102,8 @@ def save!(**options) # persisted). Returns the frozen instance. # # The record is simply removed, no callbacks are executed. - def delete(with_cas: false, **options) - options[:cas] = @__metadata__.cas if with_cas + def delete(**options) + options[:cas] = @__metadata__.cas if options.delete(:with_cas) CouchbaseOrm.logger.debug "Data - Delete #{self.id}" self.class.collection.remove(self.id, **options) @@ -119,14 +120,14 @@ def delete(with_cas: false, **options) # that no changes should be made (since they can't be persisted). # # There's a series of callbacks associated with #destroy. - def destroy(with_cas: false, **options) + def destroy(**options) return self if destroyed? raise 'model not persisted' unless persisted? run_callbacks :destroy do destroy_associations! - options[:cas] = @__metadata__.cas if with_cas + options[:cas] = @__metadata__.cas if options.delete(:with_cas) CouchbaseOrm.logger.debug "Data - Destroy #{id}" self.class.collection.remove(id, **options) @@ -225,15 +226,17 @@ def touch(**options) self end + def create_or_update(**, &block) + self.new_record? ? _create_record(&block) : _update_record(&block) + end - - def _update_record(*_args, with_cas: false, **options) - return false unless perform_validations(:update, options) + def _update_record(*, &block) return true unless changed? || self.class.attribute_types.any? { |_, type| type.is_a?(CouchbaseOrm::Types::Nested) || type.is_a?(CouchbaseOrm::Types::Array) } run_callbacks :update do run_callbacks :save do - options[:cas] = @__metadata__.cas if with_cas + options = {} + options[:cas] = @__metadata__.cas if @_with_cas CouchbaseOrm.logger.debug { "_update_record - replace #{id} #{serialized_attributes.to_s.truncate(200)}" } resp = self.class.collection.replace(id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options)) @@ -245,14 +248,13 @@ def _update_record(*_args, with_cas: false, **options) end end end - def _create_record(*_args, **options) - return false unless perform_validations(:create, options) + def _create_record(*, &block) run_callbacks :create do run_callbacks :save do assign_attributes(id: self.class.uuid_generator.next(self)) unless self.id CouchbaseOrm.logger.debug { "_create_record - Upsert #{id} #{serialized_attributes.to_s.truncate(200)}" } - resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) + resp = self.class.collection.upsert(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new) # Ensure the model is up to date @__metadata__.cas = resp.cas @@ -262,10 +264,5 @@ def _create_record(*_args, **options) end end end - - def perform_validations(context, options = {}) - return valid?(context) if options[:validate] != false - true - end end end diff --git a/spec/persistence_spec.rb b/spec/persistence_spec.rb index ff5b02c6..13537f5c 100644 --- a/spec/persistence_spec.rb +++ b/spec/persistence_spec.rb @@ -294,6 +294,16 @@ class ModelWithValidations < CouchbaseOrm::Base }) }.to raise_error(ActiveModel::UnknownAttributeError) end + it "should not perform validation with validate true" do + model = ModelWithValidations.new + + expect(model.valid?).to be(false) + expect(model.save(validate: false)).to be(true) + expect(model.persisted?).to be(true) + + model.destroy + end + describe BasicModel do it_behaves_like "ActiveModel" end