couch_potato 1.7.1 → 1.9.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.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +38 -0
  3. data/.gitignore +3 -0
  4. data/CHANGES.md +169 -131
  5. data/Gemfile +4 -0
  6. data/README.md +55 -75
  7. data/Rakefile +11 -10
  8. data/couch_potato-rspec.gemspec +2 -1
  9. data/couch_potato.gemspec +8 -6
  10. data/gemfiles/active_support_5_0 +1 -5
  11. data/gemfiles/active_support_5_1 +7 -0
  12. data/gemfiles/active_support_5_2 +7 -0
  13. data/gemfiles/active_support_6_0 +7 -0
  14. data/gemfiles/active_support_6_1 +7 -0
  15. data/lib/couch_potato/database.rb +165 -70
  16. data/lib/couch_potato/persistence/dirty_attributes.rb +3 -21
  17. data/lib/couch_potato/persistence/magic_timestamps.rb +3 -3
  18. data/lib/couch_potato/persistence/properties.rb +15 -10
  19. data/lib/couch_potato/persistence/simple_property.rb +0 -4
  20. data/lib/couch_potato/persistence/type_caster.rb +9 -6
  21. data/lib/couch_potato/persistence.rb +0 -1
  22. data/lib/couch_potato/railtie.rb +6 -11
  23. data/lib/couch_potato/validation.rb +8 -0
  24. data/lib/couch_potato/version.rb +1 -1
  25. data/lib/couch_potato/view/base_view_spec.rb +8 -32
  26. data/lib/couch_potato/view/custom_views.rb +4 -3
  27. data/lib/couch_potato/view/flex_view_spec.rb +121 -0
  28. data/lib/couch_potato/view/view_parameters.rb +34 -0
  29. data/lib/couch_potato.rb +32 -9
  30. data/spec/callbacks_spec.rb +45 -19
  31. data/spec/conflict_handling_spec.rb +0 -1
  32. data/spec/property_spec.rb +2 -2
  33. data/spec/railtie_spec.rb +10 -0
  34. data/spec/spec_helper.rb +4 -3
  35. data/spec/unit/active_model_compliance_spec.rb +7 -3
  36. data/spec/unit/attributes_spec.rb +1 -1
  37. data/spec/unit/caching_spec.rb +105 -0
  38. data/spec/unit/couch_potato_spec.rb +70 -5
  39. data/spec/unit/create_spec.rb +5 -4
  40. data/spec/unit/database_spec.rb +235 -135
  41. data/spec/unit/dirty_attributes_spec.rb +5 -26
  42. data/spec/unit/flex_view_spec_spec.rb +17 -0
  43. data/spec/unit/model_view_spec_spec.rb +1 -1
  44. data/spec/unit/rspec_stub_db_spec.rb +31 -0
  45. data/spec/unit/validation_spec.rb +42 -2
  46. data/spec/views_spec.rb +214 -103
  47. data/vendor/pouchdb-collate/LICENSE +202 -0
  48. data/vendor/pouchdb-collate/pouchdb-collate.js +430 -0
  49. metadata +46 -42
  50. data/.ruby-version +0 -1
  51. data/.travis.yml +0 -21
  52. data/gemfiles/active_support_4_0 +0 -11
  53. data/gemfiles/active_support_4_1 +0 -11
  54. data/gemfiles/active_support_4_2 +0 -11
  55. data/lib/couch_potato/persistence/deep_dirty_attributes.rb +0 -180
  56. data/spec/unit/deep_dirty_attributes_spec.rb +0 -434
data/README.md CHANGED
@@ -8,7 +8,6 @@
8
8
 
9
9
  [![Code Climate](https://codeclimate.com/github/langalex/couch_potato.png)](https://codeclimate.com/github/langalex/couch_potato)
10
10
 
11
-
12
11
  ### Mission
13
12
 
14
13
  The goal of Couch Potato is to create a minimal framework in order to store and retrieve Ruby objects to/from CouchDB and create and query views.
@@ -21,9 +20,9 @@ Lastly Couch Potato aims to provide a seamless integration with Ruby on Rails, e
21
20
 
22
21
  ### Core Features
23
22
 
24
- * persisting objects by including the CouchPotato::Persistence module
25
- * declarative views with either custom or generated map/reduce functions
26
- * extensive spec suite
23
+ - persisting objects by including the CouchPotato::Persistence module
24
+ - declarative views with either custom or generated map/reduce functions
25
+ - extensive spec suite
27
26
 
28
27
  ### Supported Environments
29
28
 
@@ -81,6 +80,13 @@ Another switch allows you to store each CouchDB view in its own design document.
81
80
  CouchPotato::Config.split_design_documents_per_view = true
82
81
  ```
83
82
 
83
+ If you are using more than one database from your app, you can create aliases:
84
+
85
+ ```ruby
86
+ CouchPotato::Config.additional_databases = {'db1' => 'db1_production', 'db2' => 'https://db2.example.com/db'}
87
+ db1 = CouchPotato.use 'db1'
88
+ ```
89
+
84
90
  #### Using with Rails
85
91
 
86
92
  Create a `config/couchdb.yml`:
@@ -90,6 +96,7 @@ default: &default
90
96
  split_design_documents_per_view: true # optional, default is false
91
97
  digest_view_names: true # optional, default is false
92
98
  default_language: :erlang # optional, default is javascript
99
+ database_host: "http://127.0.0.1:5984"
93
100
 
94
101
  development:
95
102
  <<: *default
@@ -100,9 +107,12 @@ test:
100
107
  production:
101
108
  <<: *default
102
109
  database: <%= ENV['DB_NAME'] %>
110
+ additional_databases:
111
+ db1: db1_production
112
+ db2: https://db2.example.com/db
103
113
  ```
104
114
 
105
- #### Rails 3.x
115
+ #### Rails
106
116
 
107
117
  Add to your `Gemfile`:
108
118
 
@@ -173,7 +183,6 @@ end
173
183
 
174
184
  With this in place when you set the user's age as a String (e.g. using an HTML form) it will be converted into a `Integer` automatically.
175
185
 
176
-
177
186
  Properties can have a default value:
178
187
 
179
188
  ```ruby
@@ -210,6 +219,20 @@ end
210
219
 
211
220
  When a conflict occurs Couch Potato automatically reloads the document, runs the block and tries to save it again. Note that the block is also run before initally saving the document.
212
221
 
222
+ #### Caching load reqeusts
223
+
224
+ You can add a cache to a database instance to enable caching subsequent `#load` calls to the same id.
225
+ Any write operation will completely clear the cache.
226
+
227
+ ```ruby
228
+ db = CouchPotato.database
229
+ db.cache = {}
230
+ db.load '1'
231
+ db.load '1' # goes to the cache instead of to the database
232
+ ```
233
+
234
+ In web apps, the idea is to use a per request cache, i.e. set a new cache for every request.
235
+
213
236
  #### Operations on multiple documents
214
237
 
215
238
  You can also load a bunch of documents with one request.
@@ -252,7 +275,7 @@ end
252
275
 
253
276
  #### Dirty tracking
254
277
 
255
- CouchPotato tracks the dirty state of attributes in the same way ActiveRecord does:
278
+ CouchPotato tracks the dirty state of attributes in the same way ActiveRecord does. Models are always saved though to avoid missing updates when multiple processes update the same documents concurrently.
256
279
 
257
280
  ```ruby
258
281
  user = User.create :name => 'joe'
@@ -261,68 +284,6 @@ user.name_changed? # => false
261
284
  user.name_was # => nil
262
285
  ```
263
286
 
264
- You can also force a dirty state:
265
-
266
- ```ruby
267
- user.name = 'jane'
268
- user.name_changed? # => true
269
- user.name_not_changed
270
- user.name_changed? # => false
271
- CouchPotato.database.save_document user # does nothing as no attributes are dirty
272
- ```
273
-
274
- #### Optional Deep Dirty Tracking
275
-
276
- In addition to standard dirty tracking, you can opt-in to more advanced dirty tracking for deeply structured documents by including the `CouchPotato::DeepDirtyAttributes` module in your models. This provides two benefits:
277
-
278
- 1. Dirty checking for array and embedded document properties is more reliable, such that modifying elements in an array (by any means) or changing a property of an embedded document will make the root document be `changed?`. With standard dirty checking, the `#{property}=` method must be called on the root document for it to be `changed?`.
279
- 2. It gives more useful and detailed change tracking for embedded documents, arrays of simple values, and arrays of embedded documents.
280
-
281
- The `#{property}_changed?` and `#{property}_was` methods work the same as basic dirty checking, and the `_was` values are always deep clones of the original/previous value. The `#{property}_change` and `changes` methods differ from basic dirty checking for embedded documents and arrays, giving richer details of the changes instead of just the previous and current values. This makes generating detailed, human friendly audit trails of documents easy.
282
-
283
- Tracking changes in embedded documents gives easy access to the changes in that document:
284
-
285
- ```ruby
286
- book = Book.new(:cover => Cover.new(:color => "red"))
287
- book.cover.color = "blue"
288
- book.cover_changed? # => true
289
- book.cover_was # => <deep clone of original state of book.cover>
290
- book.cover_change # => [<deep clone of original state of book.cover>, {:color => ["red", "blue"]}]
291
- ```
292
-
293
- Tracking changes in arrays of simple properties gives easy access to added and removed items:
294
-
295
- ```ruby
296
- book = Book.new(:authors => ["Sarah", "Jane"])
297
- book.authors.delete "Jane"
298
- book.authors << "Sue"
299
- book.authors_changed? # => true
300
- book.authors_was # => ["Sarah", "Jane"]
301
- book.authors_change # => [["Sarah", "Jane"], {:added => ["Sue"], :removed => ["Jane"]}]
302
- ```
303
-
304
- Tracking changes in an array of embedded documents also gives changed items:
305
-
306
- ```ruby
307
- book = Book.new(:pages => [Page.new(:number => 1), Page.new(:number => 2)]
308
- book.pages[0].title = "New title"
309
- book.pages.delete_at 1
310
- book.pages << Page.new(:number => 3)
311
- book.pages_changed? # => true
312
- book.pages_was # => <deep clone of original pages array>
313
- book.pages_change[0] # => <deep clone of original pages array>
314
- book.pages_change[1] # => {:added => [<page 3>], :removed => [<page 2>], :changed => [[<deep clone of original page 1>, {:title => [nil, "New title"]}]]}
315
- ```
316
-
317
- For change tracking in nested documents and document arrays to work, the embedded documents **must** have unique `_id` values. This can be accomplished easily in your embedded CouchPotato models by overriding `initialize`:
318
-
319
- ```ruby
320
- def initialize(*args)
321
- self._id = SecureRandom.uuid
322
- super
323
- end
324
- ```
325
-
326
287
  #### Object validations
327
288
 
328
289
  Couch Potato by default uses ActiveModel for validation
@@ -359,6 +320,16 @@ CouchPotato.database.view User.all
359
320
 
360
321
  This will load all user documents in your database sorted by `created_at`.
361
322
 
323
+ For large data sets, use batches:
324
+
325
+ ```ruby
326
+ CouchPotato.database.view_in_batches(User.all, batch_size: 100) do |users|
327
+ ...
328
+ end
329
+ ```
330
+
331
+ This will query CouchDB with skip/limit until all documents have been yielded.
332
+
362
333
  ```ruby
363
334
  CouchPotato.database.view User.all(:key => (Time.now- 10)..(Time.now), :descending => true)
364
335
  ```
@@ -451,7 +422,16 @@ end
451
422
  When querying this view you will get the raw data returned by CouchDB which looks something like this:
452
423
 
453
424
  ```json
454
- {'total_entries': 2, 'rows': [{'value': 'alex', 'key': '2009-01-03 00:02:34 +000', 'id': '75976rgi7546gi02a'}]}
425
+ {
426
+ "total_entries": 2,
427
+ "rows": [
428
+ {
429
+ "value": "alex",
430
+ "key": "2009-01-03 00:02:34 +000",
431
+ "id": "75976rgi7546gi02a"
432
+ }
433
+ ]
434
+ }
455
435
  ```
456
436
 
457
437
  To process this raw data you can also pass in a results filter:
@@ -464,7 +444,7 @@ end
464
444
 
465
445
  In this case querying the view would only return the emitted value for each row.
466
446
 
467
- You can pass in your own view specifications by passing in `:type => MyViewSpecClass`. Take a look at the CouchPotato::View::*ViewSpec classes to get an idea of how this works.
447
+ You can pass in your own view specifications by passing in `:type => MyViewSpecClass`. Take a look at the CouchPotato::View::\*ViewSpec classes to get an idea of how this works.
468
448
 
469
449
  ##### Digest view names
470
450
 
@@ -513,7 +493,6 @@ And you can pass parameters to the list:
513
493
  CouchPotato.database.view(User.all(list: :add_last_name, list_params: {filter: '*'}))
514
494
  ```
515
495
 
516
-
517
496
  #### Associations
518
497
 
519
498
  Not supported. Not sure if they ever will be. You can implement those yourself using views and custom methods on your models.
@@ -598,9 +577,9 @@ describe 'save a user' do
598
577
  end
599
578
  ```
600
579
 
601
- By creating you own instances of `CouchPotato::Database` and passing them a fake CouchRest database instance you can completely disconnect your unit tests/spec from the database.
580
+ By creating your own instances of `CouchPotato::Database` and passing them a fake CouchRest database instance you can completely disconnect your unit tests/spec from the database.
602
581
 
603
- For stubbing out the database couch potato offers some helpers via the `couch_potato-rspec` gem. Use version 2.x of the gem, you you are on RSpec 2, use 3.x for RSpec 3.
582
+ For stubbing out the database couch potato offers some helpers via the `couch_potato-rspec` gem. Use version 2.x of the gem if you are on RSpec 2, use 3.x for RSpec 3.
604
583
 
605
584
  ```ruby
606
585
  class Comment
@@ -613,7 +592,8 @@ require 'couch_potato/rspec'
613
592
  db = stub_db # stubs CouchPotato.database
614
593
  db.stub_view(Comment, :by_commenter_id).with('23').and_return([:comment1, :comment2])
615
594
 
616
- CouchPotato.database.view(Comment.by_commenter_id('23)) # => [:comment1, :comment2]
595
+ CouchPotato.database.view(Comment.by_commenter_id('23')) # => [:comment1, :comment2]
596
+ CouchPotato.database.view_in_batches(Comment.by_commenter_id('23'), batch_size: 1) # => yields [:comment1] and [:comment2]
617
597
  CouchPotato.database.first(Comment.by_commenter_id('23)) # => :comment1
618
598
  ```
619
599
 
data/Rakefile CHANGED
@@ -19,17 +19,18 @@ RSpec::Core::RakeTask.new(:spec_unit) do |spec|
19
19
  end
20
20
 
21
21
  desc 'Run all specs'
22
+ task :spec_ci do
23
+ Rake::Task[:spec_unit].execute
24
+ Rake::Task[:spec_functional].execute
25
+ end
26
+
27
+ desc 'Run all specs for all gemfiles'
22
28
  task :spec do
23
- if ENV['TRAVIS'] # travis handles the environments for us
24
- Rake::Task[:spec_unit].execute
25
- Rake::Task[:spec_functional].execute
26
- else
27
- %w(4_0 4_1 4_2 5_0).each do |version|
28
- Bundler.with_clean_env do
29
- puts "Running tests with ActiveSupport #{version.sub('_', '.')}"
30
- sh "env BUNDLE_GEMFILE=gemfiles/active_support_#{version} bundle install"
31
- sh "env BUNDLE_GEMFILE=gemfiles/active_support_#{version} bundle exec rake spec_unit spec_functional"
32
- end
29
+ %w(6_1 6_0 5_2 5_1 5_0).each do |version|
30
+ Bundler.with_clean_env do
31
+ puts "Running tests with ActiveSupport #{version.sub('_', '.')}"
32
+ sh "env BUNDLE_GEMFILE=gemfiles/active_support_#{version} bundle install"
33
+ sh "env BUNDLE_GEMFILE=gemfiles/active_support_#{version} bundle exec rake spec_unit spec_functional"
33
34
  end
34
35
  end
35
36
  end
@@ -13,8 +13,9 @@ Gem::Specification.new do |s|
13
13
 
14
14
  s.add_dependency 'rspec', '~>3.4'
15
15
  s.add_development_dependency 'rake'
16
+ s.add_dependency 'execjs', '~>2.7.0'
16
17
 
17
- s.files = `git ls-files | grep "lib/couch_potato/rspec"`.split("\n")
18
+ s.files = `git ls-files | grep "lib/couch_potato/rspec\|vendor/pouchdb-collate"`.split("\n")
18
19
  s.test_files = `git ls-files -- {test,spec,features}/* | grep rspec_matchers`.split("\n")
19
20
  s.require_paths = ['lib']
20
21
  end
data/couch_potato.gemspec CHANGED
@@ -1,4 +1,6 @@
1
- $LOAD_PATH.push File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
4
  require 'couch_potato/version'
3
5
 
4
6
  Gem::Specification.new do |s|
@@ -11,17 +13,17 @@ Gem::Specification.new do |s|
11
13
  s.version = CouchPotato::VERSION
12
14
  s.platform = Gem::Platform::RUBY
13
15
 
14
- s.add_dependency 'json', '~> 1.6'
16
+ s.add_dependency 'activemodel', ['>= 5.0', '< 7.0']
15
17
  s.add_dependency 'couchrest', '~>2.0.0'
16
- s.add_dependency 'activemodel', ['>= 4.0', '< 6.0']
18
+ s.add_dependency 'json', '~> 2.3'
17
19
 
18
- s.add_development_dependency 'rspec', '~>3.2.0'
20
+ s.add_development_dependency 'rake', '~>12.0'
21
+ s.add_development_dependency 'rspec', '~>3.5.0'
19
22
  s.add_development_dependency 'timecop'
20
23
  s.add_development_dependency 'tzinfo'
21
- s.add_development_dependency 'rake', '< 11.0'
22
24
 
23
25
  s.files = `git ls-files | grep -v "lib/couch_potato/rspec"`.split("\n")
24
26
  s.test_files = `git ls-files -- {test,spec,features}/* | grep -v rspec_matchers`.split("\n")
25
- s.executables = `git ls-files -- bin/*`.split("\n").map {|f| File.basename(f) }
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
26
28
  s.require_paths = ['lib']
27
29
  end
@@ -2,10 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  gem 'activemodel', '~>5.0.0'
4
4
  gem 'rails', '~>5.0.0'
5
- if RUBY_PLATFORM =~ /java/
6
- gem 'therubyrhino'
7
- else
8
- gem 'therubyracer'
9
- end
5
+ gem 'execjs'
10
6
 
11
7
  gemspec name: 'couch_potato', path: '..'
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activemodel', '~>5.1.7'
4
+ gem 'rails', '~>5.1.7'
5
+ gem 'execjs'
6
+
7
+ gemspec name: 'couch_potato', path: '..'
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activemodel', '~>5.2.3'
4
+ gem 'rails', '~>5.2.3'
5
+ gem 'execjs'
6
+
7
+ gemspec name: 'couch_potato', path: '..'
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activemodel', '~>6.0.2.2'
4
+ gem 'rails', '~>6.0.2.2'
5
+ gem 'execjs'
6
+
7
+ gemspec name: 'couch_potato', path: '..'
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activemodel', '~>6.1'
4
+ gem 'rails', '~>6.1'
5
+ gem 'execjs'
6
+
7
+ gemspec name: 'couch_potato', path: '..'