couch_potato 1.19.1 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebc35d2d68e09d52bac2dec8d876e6cc0173bc4f0959c3ecff6d35483791553d
4
- data.tar.gz: 0a7f6752cb79df5f1a9f6bb7a4a5252df570ddc82f4632ac21c6b6daf919263e
3
+ metadata.gz: 3550e8fe5b60fb064f2960a62313ee43e41e28c980c7c7d395d6c252fdb4fa20
4
+ data.tar.gz: 4aff0328b8e0e2162793c8b7f81643a5a3a43b703a64b00c57f044c12aac0ab0
5
5
  SHA512:
6
- metadata.gz: 4c42c9fbc4f3401e117c29fbbe7c78df593124dce08d50471b210f9b7dfb7aa5e9f6923935d96e3cb6a8fffff2e565a2fd36ab4fe935b1c492a0b7f7fb8a4159
7
- data.tar.gz: e6e43661a6e9d4c1e2d89751a2560e5cbb4fe6b311643d438e64c7c40b243b208b859c711537f1ae105bf0777185769b294c48a6f70816a9b78b4592e79f65f8
6
+ metadata.gz: 676096f6f62795015a3bb26b6fe27689eda09aacb59f05c1f181a2922bc968fd762a9571ef02085425071212cf3ee1c688d36caaf06ee68dafe134d6bf9c04ae
7
+ data.tar.gz: 49582697b2508b97fc73a9a1a0590e6882f138577ea5ab6492746a61ab654b74170830cf9c61b4de1b36f002bd344b0b193f903dec6dc78e10f6a3f5d2c7c9d1
@@ -26,7 +26,7 @@ jobs:
26
26
  steps:
27
27
  - uses: actions/checkout@v4
28
28
  - name: Set up CouchDB
29
- uses: cobot/couchdb-action@v5
29
+ uses: cobot/couchdb-action@v5.0.1
30
30
  with:
31
31
  couchdb-version: "2.3.1"
32
32
  - name: Set up Ruby
data/CHANGES.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Changes
2
2
 
3
+ # 1.20.0
4
+
5
+ - add passing validation context to save methods
6
+
7
+ # 1.19.2
8
+
9
+ - fix 404/409 error when querying single design doc with `digest_view_names` enabled
10
+
3
11
  # 1.19.1
4
12
 
5
13
  - add digest to design doc name if `single_design_document` and `digest_view_names` are enabled
@@ -105,17 +105,20 @@ module CouchPotato
105
105
  end
106
106
 
107
107
  # saves a document. returns true on success, false on failure.
108
+ # By default validations are run before saving. You can disable
109
+ # validations by passing validate: false as an option.
110
+ # You can also pass a custom validation context by passing context: :custom_context
108
111
  # if passed a block will:
109
112
  # * yield the object to be saved to the block and run if once before saving
110
113
  # * on conflict: reload the document, run the block again and retry saving
111
- def save_document(document, validate = true, retries = 0, &block)
114
+ def save_document(document, options = {}, retries = 0, &block)
112
115
  cache&.clear
113
116
  begin
114
117
  block&.call document
115
- save_document_without_conflict_handling(document, validate)
118
+ save_document_without_conflict_handling(document, options)
116
119
  rescue CouchRest::Conflict
117
120
  if block
118
- handle_write_conflict document, validate, retries, &block
121
+ handle_write_conflict document, options, retries, &block
119
122
  else
120
123
  raise CouchPotato::Conflict
121
124
  end
@@ -124,8 +127,8 @@ module CouchPotato
124
127
  alias save save_document
125
128
 
126
129
  # saves a document, raises a CouchPotato::Database::ValidationsFailedError on failure
127
- def save_document!(document)
128
- save_document(document) || raise(ValidationsFailedError, document.errors.full_messages)
130
+ def save_document!(document, options = {})
131
+ save_document(document, options) || raise(ValidationsFailedError, document.errors.full_messages)
129
132
  end
130
133
  alias save! save_document!
131
134
 
@@ -293,7 +296,7 @@ module CouchPotato
293
296
  spec.send(:klass).to_s + spec.view_name.to_s + spec.view_parameters.to_s
294
297
  end
295
298
 
296
- def handle_write_conflict(document, validate, retries, &block)
299
+ def handle_write_conflict(document, options, retries, &block)
297
300
  cache&.clear
298
301
  if retries == 5
299
302
  raise CouchPotato::Conflict
@@ -301,7 +304,7 @@ module CouchPotato
301
304
  reloaded = document.reload
302
305
  document.attributes = reloaded.attributes
303
306
  document._rev = reloaded._rev
304
- save_document document, validate, retries + 1, &block
307
+ save_document document, options, retries + 1, &block
305
308
  end
306
309
  end
307
310
 
@@ -314,11 +317,11 @@ module CouchPotato
314
317
  document._rev = nil
315
318
  end
316
319
 
317
- def save_document_without_conflict_handling(document, validate = true)
320
+ def save_document_without_conflict_handling(document, options = {})
318
321
  if document.new?
319
- create_document(document, validate)
322
+ create_document(document, options)
320
323
  else
321
- update_document(document, validate)
324
+ update_document(document, options)
322
325
  end
323
326
  end
324
327
 
@@ -337,14 +340,15 @@ module CouchPotato
337
340
  end
338
341
  end
339
342
 
340
- def create_document(document, validate)
343
+ def create_document(document, options)
341
344
  document.database = self
345
+ validate, validation_context = parse_save_options(options)
342
346
 
343
347
  if validate
344
348
  document.errors.clear
345
349
  return false if document.run_callbacks(:validation_on_save) do
346
350
  return false if document.run_callbacks(:validation_on_create) do
347
- return false unless valid_document?(document)
351
+ return false unless valid_document?(document, validation_context)
348
352
  end == false
349
353
  end == false
350
354
  end
@@ -360,12 +364,25 @@ module CouchPotato
360
364
  true
361
365
  end
362
366
 
363
- def update_document(document, validate)
367
+ def parse_save_options(options)
368
+ if options.is_a?(Hash)
369
+ validate = options.fetch(:validate, true)
370
+ validation_context = options[:context]
371
+ else
372
+ validate = !!options
373
+ validation_context = nil
374
+ end
375
+ [validate, validation_context]
376
+ end
377
+
378
+ def update_document(document, options)
379
+ validate, validation_context = parse_save_options(options)
380
+
364
381
  if validate
365
382
  document.errors.clear
366
383
  return false if document.run_callbacks(:validation_on_save) do
367
384
  return false if document.run_callbacks(:validation_on_update) do
368
- return false unless valid_document?(document)
385
+ return false unless valid_document?(document, validation_context)
369
386
  end == false
370
387
  end == false
371
388
  end
@@ -380,9 +397,9 @@ module CouchPotato
380
397
  true
381
398
  end
382
399
 
383
- def valid_document?(document)
400
+ def valid_document?(document, validation_context = nil)
384
401
  original_errors_hash = document.errors.to_hash
385
- document.valid?
402
+ document.valid?(validation_context)
386
403
  original_errors_hash.each do |k, v|
387
404
  if v.respond_to?(:each)
388
405
  v.each { |message| document.errors.add(k, message) }
@@ -1,4 +1,4 @@
1
1
  module CouchPotato
2
- VERSION = '1.19.1'.freeze
2
+ VERSION = '1.20.0'.freeze
3
3
  RSPEC_VERSION = '4.2.0'.freeze
4
4
  end
@@ -25,6 +25,8 @@ module CouchPotato
25
25
  # only after clearing the cache design docs will be updated/re-created.
26
26
  def self.clear_cache
27
27
  __updated_views.clear
28
+ @all_views = nil
29
+ @all_views_digest = nil
28
30
  end
29
31
 
30
32
  def self.__updated_views
@@ -32,53 +34,62 @@ module CouchPotato
32
34
  @updated_views
33
35
  end
34
36
 
37
+ def self.all_views
38
+ @all_views ||= CouchPotato.views.flat_map do |klass|
39
+ specs = klass.views.map { |view_name, view| klass.execute_view(view_name, {}) }
40
+ specs.map do |klass_spec|
41
+ { klass_spec.view_name => view_functions(klass_spec.map_function, klass_spec.reduce_function) }
42
+ end
43
+ end.inject(&:merge)
44
+ end
45
+
46
+ def self.all_views_digest
47
+ @all_views_digest ||= Digest::SHA256.hexdigest(all_views.to_json)
48
+ end
49
+
35
50
  private
36
51
 
37
52
  def update_view
38
- design_doc = @database.get "_design/#{@design_document_name}" rescue nil
53
+ design_doc = @database.get "_design/#{design_document_name}" rescue nil
39
54
  original_views = design_doc && design_doc['views'].dup
40
55
  view_updated
41
56
  design_doc ||= empty_design_document
42
57
  if CouchPotato::Config.single_design_document
43
- design_doc['views'] = all_views
44
- if CouchPotato::Config.digest_view_names
45
- design_doc['_id'] = "_design/#{@design_document_name}-#{Digest::SHA256.hexdigest(design_doc['views'].to_json)}"
46
- end
58
+ design_doc['views'] = self.class.all_views
47
59
  else
48
- design_doc['views'][@view_name.to_s] = view_functions
60
+ design_doc['views'][@view_name.to_s] = self.class.view_functions(@map_function, @reduce_function)
49
61
  end
50
62
  if original_views != design_doc['views']
51
63
  @database.save_doc(design_doc)
52
64
  end
53
65
  end
54
66
 
55
- def all_views
56
- CouchPotato.views.flat_map do |klass|
57
- specs = klass.views.map { |view_name, view| klass.execute_view(view_name, {}) }
58
- specs.map do |klass_spec|
59
- { klass_spec.view_name => view_functions(klass_spec.map_function, klass_spec.reduce_function) }
60
- end
61
- end.inject(&:merge)
62
- end
63
-
64
- def view_functions(map_function = @map_function, reduce_function = @reduce_function)
67
+ def self.view_functions(map_function, reduce_function)
65
68
  {'map' => map_function, 'reduce' => reduce_function}.compact
66
69
  end
67
70
 
68
71
  def empty_design_document
69
- {'views' => {}, "_id" => "_design/#{@design_document_name}", "language" => @language.to_s}
72
+ {'views' => {}, "_id" => "_design/#{design_document_name}", "language" => @language.to_s}
73
+ end
74
+
75
+ def design_document_name
76
+ name = @design_document_name
77
+ if CouchPotato::Config.digest_view_names && CouchPotato::Config.single_design_document
78
+ name += "-#{self.class.all_views_digest}"
79
+ end
80
+ name
70
81
  end
71
82
 
72
83
  def view_has_been_updated?
73
84
  if CouchPotato::Config.single_design_document
74
85
  updated_views.any?
75
86
  else
76
- updated_views[[@design_document_name, @view_name]]
87
+ updated_views[[design_document_name, @view_name]]
77
88
  end
78
89
  end
79
90
 
80
91
  def view_updated
81
- updated_views[[@design_document_name, @view_name]] = true
92
+ updated_views[[design_document_name, @view_name]] = true
82
93
  end
83
94
 
84
95
  def updated_views
@@ -90,7 +101,7 @@ module CouchPotato
90
101
  end
91
102
 
92
103
  def view_url
93
- "#{@design_document_name}/#{@view_name}"
104
+ "#{design_document_name}/#{@view_name}"
94
105
  end
95
106
  end
96
107
  end
@@ -30,10 +30,13 @@ describe 'single design document' do
30
30
  recreate_db
31
31
  CouchPotato::Config.single_design_document = true
32
32
  CouchPotato.views.select! { |v| [Thing1, Thing2, Thing3].include?(v) } # clear classes from other specs
33
+ CouchPotato::View::ViewQuery.clear_cache
33
34
  end
34
35
 
35
36
  after(:each) do
36
37
  CouchPotato::Config.single_design_document = false
38
+ CouchPotato::Config.digest_view_names = false
39
+ CouchPotato::View::ViewQuery.clear_cache
37
40
  end
38
41
 
39
42
  it 'creates a single design document for all views' do
@@ -60,4 +63,13 @@ describe 'single design document' do
60
63
  expect(db.view(Thing2.all('n2'))).to eq([thing2])
61
64
  expect(db.view(Thing3.by_tag('tag1'))).to eq([thing3])
62
65
  end
66
+
67
+ it 'queries the single design doc with digest_view_names enabled' do
68
+ CouchPotato::Config.digest_view_names = true
69
+
70
+ thing1 = Thing1.new title: 't1'
71
+ db.save! thing1
72
+
73
+ expect(db.view(Thing1.all)).to(eq([thing1]))
74
+ end
63
75
  end
data/spec/spec_helper.rb CHANGED
@@ -39,6 +39,16 @@ class BigDecimalContainer
39
39
  property :number, type: BigDecimal
40
40
  end
41
41
 
42
+ class WithValidationContext
43
+ include CouchPotato::Persistence
44
+
45
+ property :name
46
+
47
+ validates_presence_of :name, on: :create
48
+ validates_length_of :name, minimum: 5, on: :update
49
+ validates_length_of :name, minimum: 10, on: :custom
50
+ end
51
+
42
52
  def recreate_db
43
53
  CouchPotato.couchrest_database.recreate!
44
54
  end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ describe "validation context" do
4
+ let(:db) { CouchPotato.database }
5
+
6
+ context 'when calling save' do
7
+
8
+ it 'uses the :create context on creation' do
9
+ model = WithValidationContext.new
10
+
11
+ db.save(model)
12
+
13
+ expect(model.errors[:name]).to eq(["can't be blank"])
14
+ end
15
+
16
+ it 'uses the :update context on update' do
17
+ model = WithValidationContext.new(name: 'initial name')
18
+ db.save!(model)
19
+
20
+ model.name = 'new'
21
+ db.save(model)
22
+
23
+ expect(model.errors[:name]).to eq(["is too short (minimum is 5 characters)"])
24
+ end
25
+
26
+ it 'uses a custom context on create when specified' do
27
+ model = WithValidationContext.new(name: 'short')
28
+
29
+ db.save(model, context: :custom)
30
+
31
+ expect(model.errors[:name]).to eq(["is too short (minimum is 10 characters)"])
32
+ end
33
+
34
+ it 'uses a custom context on update when specified' do
35
+ model = WithValidationContext.new(name: 'initial name')
36
+ db.save!(model)
37
+
38
+ model.name = 'new'
39
+ db.save(model, context: :custom)
40
+
41
+ expect(model.errors[:name]).to eq(["is too short (minimum is 10 characters)"])
42
+ end
43
+
44
+ end
45
+
46
+ context 'when calling save!' do
47
+
48
+ it 'uses the :create context on creation' do
49
+ model = WithValidationContext.new
50
+
51
+ expect do
52
+ db.save!(model)
53
+ end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name can't be blank/)
54
+ end
55
+
56
+ it 'uses the :update context on update' do
57
+ model = WithValidationContext.new(name: 'initial name')
58
+ db.save!(model)
59
+
60
+ model.name = 'new'
61
+ expect do
62
+ db.save!(model)
63
+ end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name is too short \(minimum is 5 characters\)/)
64
+ end
65
+
66
+ it 'uses a custom context on create when specified' do
67
+ model = WithValidationContext.new(name: 'short')
68
+
69
+ expect do
70
+ db.save!(model, context: :custom)
71
+ end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name is too short \(minimum is 10 characters\)/)
72
+ end
73
+
74
+ it 'uses a custom context on update when specified' do
75
+ model = WithValidationContext.new(name: 'initial name')
76
+ db.save!(model)
77
+
78
+ model.name = 'new'
79
+ expect do
80
+ db.save!(model, context: :custom)
81
+ end.to raise_error(CouchPotato::Database::ValidationsFailedError, /Name is too short \(minimum is 10 characters\)/)
82
+ end
83
+ end
84
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couch_potato
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.1
4
+ version: 1.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Lang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-05 00:00:00.000000000 Z
11
+ date: 2025-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -207,6 +207,7 @@ files:
207
207
  - spec/unit/validation_spec.rb
208
208
  - spec/unit/view_query_spec.rb
209
209
  - spec/update_spec.rb
210
+ - spec/validation_context_spec.rb
210
211
  - spec/view_updates_spec.rb
211
212
  - spec/views_spec.rb
212
213
  - vendor/pouchdb-collate/LICENSE
@@ -275,5 +276,6 @@ test_files:
275
276
  - spec/unit/validation_spec.rb
276
277
  - spec/unit/view_query_spec.rb
277
278
  - spec/update_spec.rb
279
+ - spec/validation_context_spec.rb
278
280
  - spec/view_updates_spec.rb
279
281
  - spec/views_spec.rb