couch_potato 1.7.0 → 1.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +50 -0
- data/.gitignore +3 -0
- data/CHANGES.md +180 -130
- data/Gemfile +4 -0
- data/README.md +61 -85
- data/Rakefile +11 -10
- data/couch_potato-rspec.gemspec +2 -1
- data/couch_potato.gemspec +9 -7
- data/gemfiles/active_support_5_0 +6 -0
- data/gemfiles/active_support_5_1 +7 -0
- data/gemfiles/active_support_5_2 +6 -0
- data/gemfiles/active_support_6_0 +6 -0
- data/gemfiles/active_support_6_1 +6 -0
- data/gemfiles/active_support_7_0 +6 -0
- data/lib/couch_potato/database.rb +170 -71
- data/lib/couch_potato/persistence/dirty_attributes.rb +3 -21
- data/lib/couch_potato/persistence/magic_timestamps.rb +3 -3
- data/lib/couch_potato/persistence/properties.rb +15 -10
- data/lib/couch_potato/persistence/simple_property.rb +0 -4
- data/lib/couch_potato/persistence/type_caster.rb +11 -6
- data/lib/couch_potato/persistence.rb +0 -1
- data/lib/couch_potato/railtie.rb +6 -11
- data/lib/couch_potato/validation.rb +8 -0
- data/lib/couch_potato/version.rb +2 -2
- data/lib/couch_potato/view/base_view_spec.rb +8 -32
- data/lib/couch_potato/view/custom_views.rb +4 -3
- data/lib/couch_potato/view/flex_view_spec.rb +121 -0
- data/lib/couch_potato/view/view_parameters.rb +34 -0
- data/lib/couch_potato.rb +37 -16
- data/spec/callbacks_spec.rb +45 -19
- data/spec/conflict_handling_spec.rb +1 -2
- data/spec/property_spec.rb +12 -3
- data/spec/railtie_spec.rb +10 -0
- data/spec/spec_helper.rb +4 -3
- data/spec/unit/active_model_compliance_spec.rb +7 -3
- data/spec/unit/attributes_spec.rb +54 -1
- data/spec/unit/caching_spec.rb +105 -0
- data/spec/unit/couch_potato_spec.rb +70 -5
- data/spec/unit/create_spec.rb +5 -4
- data/spec/unit/database_spec.rb +239 -135
- data/spec/unit/dirty_attributes_spec.rb +5 -26
- data/spec/unit/flex_view_spec_spec.rb +17 -0
- data/spec/unit/model_view_spec_spec.rb +1 -1
- data/spec/unit/rspec_stub_db_spec.rb +31 -0
- data/spec/unit/validation_spec.rb +42 -2
- data/spec/unit/view_query_spec.rb +12 -7
- data/spec/views_spec.rb +214 -103
- data/vendor/pouchdb-collate/LICENSE +202 -0
- data/vendor/pouchdb-collate/pouchdb-collate.js +430 -0
- metadata +47 -36
- data/.ruby-version +0 -1
- data/.travis.yml +0 -21
- data/gemfiles/active_support_4_0 +0 -11
- data/gemfiles/active_support_4_1 +0 -11
- data/gemfiles/active_support_4_2 +0 -11
- data/lib/couch_potato/persistence/deep_dirty_attributes.rb +0 -180
- 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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
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
|
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 `
|
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 =>
|
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 `
|
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
|
-
{
|
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
|
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
|
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
|
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 =>
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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(7_0 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
|
data/couch_potato-rspec.gemspec
CHANGED
@@ -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'
|
16
17
|
|
17
18
|
s.files = `git ls-files | grep "lib/couch_potato/rspec"`.split("\n")
|
18
|
-
s.test_files = `git ls-files --
|
19
|
+
s.test_files = `git ls-files -- spec/* | grep rspec_matchers`.split("\n")
|
19
20
|
s.require_paths = ['lib']
|
20
21
|
end
|
data/couch_potato.gemspec
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
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 '
|
15
|
-
s.add_dependency 'couchrest', '~>2.0.0
|
16
|
-
s.add_dependency '
|
16
|
+
s.add_dependency 'activemodel', ['>= 5.0', '< 7.1']
|
17
|
+
s.add_dependency 'couchrest', '~>2.0.0'
|
18
|
+
s.add_dependency 'json', '~> 2.3'
|
17
19
|
|
18
|
-
s.add_development_dependency '
|
20
|
+
s.add_development_dependency 'rake', '~>12.0'
|
21
|
+
s.add_development_dependency 'rspec', '~>3.10.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
|