Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up CouchDB
uses: cobot/couchdb-action@v5
uses: cobot/couchdb-action@v5.0.1
with:
couchdb-version: "2.3.1"
- name: Set up Ruby
Expand Down
49 changes: 33 additions & 16 deletions lib/couch_potato/database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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!

Expand Down Expand Up @@ -293,15 +296,15 @@ 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
else
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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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) }
Expand Down
10 changes: 10 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 84 additions & 0 deletions spec/validation_context_spec.rb
Original file line number Diff line number Diff line change
@@ -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