couch_potato 1.6.4 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +38 -0
  3. data/.gitignore +3 -0
  4. data/CHANGES.md +193 -122
  5. data/Gemfile +4 -0
  6. data/README.md +61 -85
  7. data/Rakefile +11 -10
  8. data/couch_potato-rspec.gemspec +3 -2
  9. data/couch_potato.gemspec +9 -7
  10. data/gemfiles/active_support_5_0 +7 -0
  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 +168 -71
  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/revisions.rb +14 -0
  20. data/lib/couch_potato/persistence/simple_property.rb +0 -4
  21. data/lib/couch_potato/persistence/type_caster.rb +11 -6
  22. data/lib/couch_potato/persistence.rb +5 -3
  23. data/lib/couch_potato/railtie.rb +7 -12
  24. data/lib/couch_potato/validation.rb +8 -0
  25. data/lib/couch_potato/version.rb +2 -2
  26. data/lib/couch_potato/view/base_view_spec.rb +8 -32
  27. data/lib/couch_potato/view/custom_views.rb +4 -3
  28. data/lib/couch_potato/view/flex_view_spec.rb +121 -0
  29. data/lib/couch_potato/view/view_parameters.rb +34 -0
  30. data/lib/couch_potato.rb +37 -16
  31. data/spec/callbacks_spec.rb +45 -19
  32. data/spec/conflict_handling_spec.rb +1 -2
  33. data/spec/property_spec.rb +12 -3
  34. data/spec/railtie_spec.rb +17 -1
  35. data/spec/revisions_spec.rb +25 -0
  36. data/spec/spec_helper.rb +4 -3
  37. data/spec/unit/active_model_compliance_spec.rb +7 -3
  38. data/spec/unit/attributes_spec.rb +54 -1
  39. data/spec/unit/caching_spec.rb +105 -0
  40. data/spec/unit/couch_potato_spec.rb +70 -5
  41. data/spec/unit/create_spec.rb +5 -4
  42. data/spec/unit/database_spec.rb +235 -135
  43. data/spec/unit/dirty_attributes_spec.rb +5 -26
  44. data/spec/unit/flex_view_spec_spec.rb +17 -0
  45. data/spec/unit/model_view_spec_spec.rb +1 -1
  46. data/spec/unit/rspec_stub_db_spec.rb +31 -0
  47. data/spec/unit/validation_spec.rb +42 -2
  48. data/spec/views_spec.rb +214 -103
  49. data/vendor/pouchdb-collate/LICENSE +202 -0
  50. data/vendor/pouchdb-collate/pouchdb-collate.js +430 -0
  51. metadata +46 -33
  52. data/.ruby-version +0 -1
  53. data/.travis.yml +0 -20
  54. data/gemfiles/active_support_4_0 +0 -11
  55. data/gemfiles/active_support_4_1 +0 -11
  56. data/gemfiles/active_support_4_2 +0 -11
  57. data/lib/couch_potato/persistence/deep_dirty_attributes.rb +0 -180
  58. 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,17 +20,13 @@ 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
 
30
- * Ruby 2.0, 2.1, 2.2, Rubinius
31
- * CouchDB 1.6.0
32
- * ActiveSupport 4.0, 4.1, 4.2
33
-
34
- (Supported means I run the specs against those before releasing a new gem.)
29
+ Check travis.yml for supported Ruby/ActiveSupport versions.
35
30
 
36
31
  ### Installation
37
32
 
@@ -85,6 +80,13 @@ Another switch allows you to store each CouchDB view in its own design document.
85
80
  CouchPotato::Config.split_design_documents_per_view = true
86
81
  ```
87
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
+
88
90
  #### Using with Rails
89
91
 
90
92
  Create a `config/couchdb.yml`:
@@ -94,6 +96,7 @@ default: &default
94
96
  split_design_documents_per_view: true # optional, default is false
95
97
  digest_view_names: true # optional, default is false
96
98
  default_language: :erlang # optional, default is javascript
99
+ database_host: "http://127.0.0.1:5984"
97
100
 
98
101
  development:
99
102
  <<: *default
@@ -104,9 +107,12 @@ test:
104
107
  production:
105
108
  <<: *default
106
109
  database: <%= ENV['DB_NAME'] %>
110
+ additional_databases:
111
+ db1: db1_production
112
+ db2: https://db2.example.com/db
107
113
  ```
108
114
 
109
- #### Rails 3.x
115
+ #### Rails
110
116
 
111
117
  Add to your `Gemfile`:
112
118
 
@@ -164,19 +170,18 @@ end
164
170
  ```
165
171
 
166
172
  In this case `Address` also implements `CouchPotato::Persistence` which means its JSON representation will be added to the user document.
167
- Couch Potato also has support for the basic types (right now `Fixnum`, `Date`, `Time` and `:boolean` are supported):
173
+ Couch Potato also has support for the basic types (right now `Integer`, `Date`, `Time` and `:boolean` are supported):
168
174
 
169
175
  ```ruby
170
176
  class User
171
177
  include CouchPotato::Persistence
172
178
 
173
- property :age, :type => Fixnum
179
+ property :age, :type => Integer
174
180
  property :receive_newsletter, :type => :boolean
175
181
  end
176
182
  ```
177
183
 
178
- 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 `Fixnum` automatically.
179
-
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.
180
185
 
181
186
  Properties can have a default value:
182
187
 
@@ -214,6 +219,20 @@ end
214
219
 
215
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.
216
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
+
217
236
  #### Operations on multiple documents
218
237
 
219
238
  You can also load a bunch of documents with one request.
@@ -256,7 +275,7 @@ end
256
275
 
257
276
  #### Dirty tracking
258
277
 
259
- 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.
260
279
 
261
280
  ```ruby
262
281
  user = User.create :name => 'joe'
@@ -265,68 +284,6 @@ user.name_changed? # => false
265
284
  user.name_was # => nil
266
285
  ```
267
286
 
268
- You can also force a dirty state:
269
-
270
- ```ruby
271
- user.name = 'jane'
272
- user.name_changed? # => true
273
- user.name_not_changed
274
- user.name_changed? # => false
275
- CouchPotato.database.save_document user # does nothing as no attributes are dirty
276
- ```
277
-
278
- #### Optional Deep Dirty Tracking
279
-
280
- 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:
281
-
282
- 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?`.
283
- 2. It gives more useful and detailed change tracking for embedded documents, arrays of simple values, and arrays of embedded documents.
284
-
285
- 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.
286
-
287
- Tracking changes in embedded documents gives easy access to the changes in that document:
288
-
289
- ```ruby
290
- book = Book.new(:cover => Cover.new(:color => "red"))
291
- book.cover.color = "blue"
292
- book.cover_changed? # => true
293
- book.cover_was # => <deep clone of original state of book.cover>
294
- book.cover_change # => [<deep clone of original state of book.cover>, {:color => ["red", "blue"]}]
295
- ```
296
-
297
- Tracking changes in arrays of simple properties gives easy access to added and removed items:
298
-
299
- ```ruby
300
- book = Book.new(:authors => ["Sarah", "Jane"])
301
- book.authors.delete "Jane"
302
- book.authors << "Sue"
303
- book.authors_changed? # => true
304
- book.authors_was # => ["Sarah", "Jane"]
305
- book.authors_change # => [["Sarah", "Jane"], {:added => ["Sue"], :removed => ["Jane"]}]
306
- ```
307
-
308
- Tracking changes in an array of embedded documents also gives changed items:
309
-
310
- ```ruby
311
- book = Book.new(:pages => [Page.new(:number => 1), Page.new(:number => 2)]
312
- book.pages[0].title = "New title"
313
- book.pages.delete_at 1
314
- book.pages << Page.new(:number => 3)
315
- book.pages_changed? # => true
316
- book.pages_was # => <deep clone of original pages array>
317
- book.pages_change[0] # => <deep clone of original pages array>
318
- book.pages_change[1] # => {:added => [<page 3>], :removed => [<page 2>], :changed => [[<deep clone of original page 1>, {:title => [nil, "New title"]}]]}
319
- ```
320
-
321
- 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`:
322
-
323
- ```ruby
324
- def initialize(*args)
325
- self._id = SecureRandom.uuid
326
- super
327
- end
328
- ```
329
-
330
287
  #### Object validations
331
288
 
332
289
  Couch Potato by default uses ActiveModel for validation
@@ -363,6 +320,16 @@ CouchPotato.database.view User.all
363
320
 
364
321
  This will load all user documents in your database sorted by `created_at`.
365
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
+
366
333
  ```ruby
367
334
  CouchPotato.database.view User.all(:key => (Time.now- 10)..(Time.now), :descending => true)
368
335
  ```
@@ -455,7 +422,16 @@ end
455
422
  When querying this view you will get the raw data returned by CouchDB which looks something like this:
456
423
 
457
424
  ```json
458
- {'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
+ }
459
435
  ```
460
436
 
461
437
  To process this raw data you can also pass in a results filter:
@@ -468,11 +444,11 @@ end
468
444
 
469
445
  In this case querying the view would only return the emitted value for each row.
470
446
 
471
- 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.
472
448
 
473
449
  ##### Digest view names
474
450
 
475
- If turned on, Couch Potato will append an MD5 digest of the map function to each view name. This makes sure (together with split_design_documents_per_view) that no views/design documents are ever updated. Instead, new ones are created. Since reindexing can take a long time once your database is larger, you want to avoid blocking your app while CouchDB is busy. Instead, you create a new view, warm it up, and only then start using it.
451
+ If turned on, Couch Potato will append an MD5 digest of the map function to each view name. This makes sure (together with split_design_documents_per_view) that no views/design documents are ever updated. Instead, new ones are created. Since reindexing can take a long time once your database is larger, you want to avoid blocking your app while CouchDB is busy. Instead, you create a new view, warm it up, and only then start using it.
476
452
 
477
453
  ##### Lists
478
454
 
@@ -517,7 +493,6 @@ And you can pass parameters to the list:
517
493
  CouchPotato.database.view(User.all(list: :add_last_name, list_params: {filter: '*'}))
518
494
  ```
519
495
 
520
-
521
496
  #### Associations
522
497
 
523
498
  Not supported. Not sure if they ever will be. You can implement those yourself using views and custom methods on your models.
@@ -602,9 +577,9 @@ describe 'save a user' do
602
577
  end
603
578
  ```
604
579
 
605
- 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.
606
581
 
607
- 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.
608
583
 
609
584
  ```ruby
610
585
  class Comment
@@ -617,7 +592,8 @@ require 'couch_potato/rspec'
617
592
  db = stub_db # stubs CouchPotato.database
618
593
  db.stub_view(Comment, :by_commenter_id).with('23').and_return([:comment1, :comment2])
619
594
 
620
- 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]
621
597
  CouchPotato.database.first(Comment.by_commenter_id('23)) # => :comment1
622
598
  ```
623
599
 
@@ -630,7 +606,7 @@ class User
630
606
  include CouchPotato::Persistence
631
607
 
632
608
  property :name
633
- property :age, :type => Fixnum
609
+ property :age, :type => Integer
634
610
 
635
611
  view :by_name, :key => :name
636
612
  view :by_age, :key => :age
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).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
@@ -11,10 +11,11 @@ Gem::Specification.new do |s|
11
11
  s.version = CouchPotato::RSPEC_VERSION
12
12
  s.platform = Gem::Platform::RUBY
13
13
 
14
- s.add_dependency 'rspec', '~>3.0'
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'
15
- s.add_dependency 'couchrest', '~>2.0.0.rc3'
16
- s.add_dependency 'activemodel', '~> 4.0'
16
+ s.add_dependency 'activemodel', ['>= 5.0', '< 7.0']
17
+ s.add_dependency 'couchrest', '~>2.0.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'
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
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activemodel', '~>5.0.0'
4
+ gem 'rails', '~>5.0.0'
5
+ gem 'execjs'
6
+
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: '..'