From 8305dd6ac1855ecd0023427f401b952dee589aac Mon Sep 17 00:00:00 2001 From: Alexander Lang Date: Tue, 9 Dec 2025 12:01:15 +0100 Subject: [PATCH 1/4] support passing validation context on save --- lib/couch_potato/database.rb | 49 ++++++++++++------- spec/spec_helper.rb | 10 ++++ spec/validation_context_spec.rb | 84 +++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 spec/validation_context_spec.rb diff --git a/lib/couch_potato/database.rb b/lib/couch_potato/database.rb index d0a17e8..d37e666 100644 --- a/lib/couch_potato/database.rb +++ b/lib/couch_potato/database.rb @@ -105,17 +105,20 @@ def first!(spec) end # saves a document. returns true on success, false on failure. + # By default validations are run before saving. You can disable + # validations by passing validate: false as an option. + # You can also pass a custom validation context by passing context: :custom_context # if passed a block will: # * yield the object to be saved to the block and run if once before saving # * on conflict: reload the document, run the block again and retry saving - def save_document(document, validate = true, retries = 0, &block) + def save_document(document, options = {}, retries = 0, &block) cache&.clear begin block&.call document - save_document_without_conflict_handling(document, validate) + save_document_without_conflict_handling(document, options) rescue CouchRest::Conflict if block - handle_write_conflict document, validate, retries, &block + handle_write_conflict document, options, retries, &block else raise CouchPotato::Conflict end @@ -124,8 +127,8 @@ def save_document(document, validate = true, retries = 0, &block) alias save save_document # saves a document, raises a CouchPotato::Database::ValidationsFailedError on failure - def save_document!(document) - save_document(document) || raise(ValidationsFailedError, document.errors.full_messages) + def save_document!(document, options = {}) + save_document(document, options) || raise(ValidationsFailedError, document.errors.full_messages) end alias save! save_document! @@ -293,7 +296,7 @@ def view_cache_id(spec) spec.send(:klass).to_s + spec.view_name.to_s + spec.view_parameters.to_s end - def handle_write_conflict(document, validate, retries, &block) + def handle_write_conflict(document, options, retries, &block) cache&.clear if retries == 5 raise CouchPotato::Conflict @@ -301,7 +304,7 @@ def handle_write_conflict(document, validate, retries, &block) reloaded = document.reload document.attributes = reloaded.attributes document._rev = reloaded._rev - save_document document, validate, retries + 1, &block + save_document document, options, retries + 1, &block end end @@ -314,11 +317,11 @@ def destroy_document_without_conflict_handling(document) document._rev = nil end - def save_document_without_conflict_handling(document, validate = true) + def save_document_without_conflict_handling(document, options = {}) if document.new? - create_document(document, validate) + create_document(document, options) else - update_document(document, validate) + update_document(document, options) end end @@ -337,14 +340,15 @@ def bulk_load(ids) end end - def create_document(document, validate) + def create_document(document, options) document.database = self + validate, validation_context = parse_save_options(options) if validate document.errors.clear return false if document.run_callbacks(:validation_on_save) do return false if document.run_callbacks(:validation_on_create) do - return false unless valid_document?(document) + return false unless valid_document?(document, validation_context) end == false end == false end @@ -360,12 +364,25 @@ def create_document(document, validate) true end - def update_document(document, validate) + def parse_save_options(options) + if options.is_a?(Hash) + validate = options.fetch(:validate, true) + validation_context = options[:context] + else + validate = !!options + validation_context = nil + end + [validate, validation_context] + end + + def update_document(document, options) + validate, validation_context = parse_save_options(options) + if validate document.errors.clear return false if document.run_callbacks(:validation_on_save) do return false if document.run_callbacks(:validation_on_update) do - return false unless valid_document?(document) + return false unless valid_document?(document, validation_context) end == false end == false end @@ -380,9 +397,9 @@ def update_document(document, validate) true end - def valid_document?(document) + def valid_document?(document, validation_context = nil) original_errors_hash = document.errors.to_hash - document.valid? + document.valid?(validation_context) original_errors_hash.each do |k, v| if v.respond_to?(:each) v.each { |message| document.errors.add(k, message) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f81721f..71e456a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -39,6 +39,16 @@ class BigDecimalContainer property :number, type: BigDecimal end +class WithValidationContext + include CouchPotato::Persistence + + property :name + + validates_presence_of :name, on: :create + validates_length_of :name, minimum: 5, on: :update + validates_length_of :name, minimum: 10, on: :custom +end + def recreate_db CouchPotato.couchrest_database.recreate! end diff --git a/spec/validation_context_spec.rb b/spec/validation_context_spec.rb new file mode 100644 index 0000000..21fceed --- /dev/null +++ b/spec/validation_context_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe "validation context" do + let(:db) { CouchPotato.database } + + context 'when calling save' do + + it 'uses the :create context on creation' do + model = WithValidationContext.new + + db.save(model) + + expect(model.errors[:name]).to eq(["can't be blank"]) + end + + it 'uses the :update context on update' do + model = WithValidationContext.new(name: 'initial name') + db.save!(model) + + model.name = 'new' + db.save(model) + + expect(model.errors[:name]).to eq(["is too short (minimum is 5 characters)"]) + end + + it 'uses a custom context on create when specified' do + model = WithValidationContext.new(name: 'short') + + db.save(model, context: :custom) + + expect(model.errors[:name]).to eq(["is too short (minimum is 10 characters)"]) + end + + it 'uses a custom context on update when specified' do + model = WithValidationContext.new(name: 'initial name') + db.save!(model) + + model.name = 'new' + db.save(model, context: :custom) + + expect(model.errors[:name]).to eq(["is too short (minimum is 10 characters)"]) + end + + end + + context 'when calling save!' do + + it 'uses the :create context on creation' do + model = WithValidationContext.new + + expect do + db.save!(model) + end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name can't be blank/) + end + + it 'uses the :update context on update' do + model = WithValidationContext.new(name: 'initial name') + db.save!(model) + + model.name = 'new' + expect do + db.save!(model) + end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name is too short \(minimum is 5 characters\)/) + end + + it 'uses a custom context on create when specified' do + model = WithValidationContext.new(name: 'short') + + expect do + db.save!(model, context: :custom) + end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name is too short \(minimum is 10 characters\)/) + end + + it 'uses a custom context on update when specified' do + model = WithValidationContext.new(name: 'initial name') + db.save!(model) + + model.name = 'new' + expect do + db.save!(model, context: :custom) + end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name is too short \(minimum is 10 characters\)/) + end + end +end \ No newline at end of file From d0ae4c258ca4382e57ac2eba1ca74ab3bf9eba08 Mon Sep 17 00:00:00 2001 From: Alexander Lang Date: Tue, 9 Dec 2025 12:14:04 +0100 Subject: [PATCH 2/4] debug: try new action version --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 43a0bf7..4c82212 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up CouchDB - uses: cobot/couchdb-action@v5 + uses: cobot/couchdb-action@716f8ccff36d9ee08e27f094d842ff8955f83c66 with: couchdb-version: "2.3.1" - name: Set up Ruby From 15490652a3acd1ff2fedba9b49ea99428a28cbfe Mon Sep 17 00:00:00 2001 From: Alexander Lang Date: Tue, 9 Dec 2025 12:18:20 +0100 Subject: [PATCH 3/4] bump version --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 4c82212..d05918d 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up CouchDB - uses: cobot/couchdb-action@716f8ccff36d9ee08e27f094d842ff8955f83c66 + uses: cobot/couchdb-action@5.0.1 with: couchdb-version: "2.3.1" - name: Set up Ruby From 7ef3837c731cde43af40c8345d2b6034a7ca629d Mon Sep 17 00:00:00 2001 From: Alexander Lang Date: Tue, 9 Dec 2025 12:21:19 +0100 Subject: [PATCH 4/4] fix version --- .github/workflows/ruby.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index d05918d..37fb20c 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up CouchDB - uses: cobot/couchdb-action@5.0.1 + uses: cobot/couchdb-action@v5.0.1 with: couchdb-version: "2.3.1" - name: Set up Ruby