openlogic-couchrest_model 1.0.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.
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/Gemfile +4 -0
- data/LICENSE +176 -0
- data/README.md +137 -0
- data/Rakefile +38 -0
- data/THANKS.md +21 -0
- data/VERSION +1 -0
- data/benchmarks/dirty.rb +118 -0
- data/couchrest_model.gemspec +36 -0
- data/history.md +309 -0
- data/init.rb +1 -0
- data/lib/couchrest/model.rb +10 -0
- data/lib/couchrest/model/associations.rb +231 -0
- data/lib/couchrest/model/base.rb +129 -0
- data/lib/couchrest/model/callbacks.rb +28 -0
- data/lib/couchrest/model/casted_array.rb +83 -0
- data/lib/couchrest/model/casted_by.rb +33 -0
- data/lib/couchrest/model/casted_hash.rb +84 -0
- data/lib/couchrest/model/class_proxy.rb +135 -0
- data/lib/couchrest/model/collection.rb +273 -0
- data/lib/couchrest/model/configuration.rb +67 -0
- data/lib/couchrest/model/connection.rb +70 -0
- data/lib/couchrest/model/core_extensions/hash.rb +9 -0
- data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
- data/lib/couchrest/model/design_doc.rb +128 -0
- data/lib/couchrest/model/designs.rb +91 -0
- data/lib/couchrest/model/designs/view.rb +513 -0
- data/lib/couchrest/model/dirty.rb +39 -0
- data/lib/couchrest/model/document_queries.rb +99 -0
- data/lib/couchrest/model/embeddable.rb +78 -0
- data/lib/couchrest/model/errors.rb +25 -0
- data/lib/couchrest/model/extended_attachments.rb +83 -0
- data/lib/couchrest/model/persistence.rb +178 -0
- data/lib/couchrest/model/properties.rb +228 -0
- data/lib/couchrest/model/property.rb +114 -0
- data/lib/couchrest/model/property_protection.rb +71 -0
- data/lib/couchrest/model/proxyable.rb +183 -0
- data/lib/couchrest/model/support/couchrest_database.rb +13 -0
- data/lib/couchrest/model/support/couchrest_design.rb +33 -0
- data/lib/couchrest/model/typecast.rb +154 -0
- data/lib/couchrest/model/validations.rb +80 -0
- data/lib/couchrest/model/validations/casted_model.rb +16 -0
- data/lib/couchrest/model/validations/locale/en.yml +5 -0
- data/lib/couchrest/model/validations/uniqueness.rb +69 -0
- data/lib/couchrest/model/views.rb +151 -0
- data/lib/couchrest/railtie.rb +24 -0
- data/lib/couchrest_model.rb +66 -0
- data/lib/rails/generators/couchrest_model.rb +16 -0
- data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
- data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
- data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
- data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
- data/spec/.gitignore +1 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/config/couchdb.yml +10 -0
- data/spec/fixtures/models/article.rb +36 -0
- data/spec/fixtures/models/base.rb +164 -0
- data/spec/fixtures/models/card.rb +19 -0
- data/spec/fixtures/models/cat.rb +23 -0
- data/spec/fixtures/models/client.rb +6 -0
- data/spec/fixtures/models/course.rb +27 -0
- data/spec/fixtures/models/event.rb +8 -0
- data/spec/fixtures/models/invoice.rb +14 -0
- data/spec/fixtures/models/key_chain.rb +5 -0
- data/spec/fixtures/models/membership.rb +4 -0
- data/spec/fixtures/models/person.rb +11 -0
- data/spec/fixtures/models/project.rb +6 -0
- data/spec/fixtures/models/question.rb +7 -0
- data/spec/fixtures/models/sale_entry.rb +9 -0
- data/spec/fixtures/models/sale_invoice.rb +14 -0
- data/spec/fixtures/models/service.rb +10 -0
- data/spec/fixtures/models/user.rb +22 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/functional/validations_spec.rb +8 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/unit/active_model_lint_spec.rb +30 -0
- data/spec/unit/assocations_spec.rb +242 -0
- data/spec/unit/attachment_spec.rb +176 -0
- data/spec/unit/base_spec.rb +537 -0
- data/spec/unit/casted_spec.rb +72 -0
- data/spec/unit/class_proxy_spec.rb +167 -0
- data/spec/unit/collection_spec.rb +86 -0
- data/spec/unit/configuration_spec.rb +77 -0
- data/spec/unit/connection_spec.rb +148 -0
- data/spec/unit/core_extensions/time_parsing.rb +77 -0
- data/spec/unit/design_doc_spec.rb +241 -0
- data/spec/unit/designs/view_spec.rb +831 -0
- data/spec/unit/designs_spec.rb +134 -0
- data/spec/unit/dirty_spec.rb +436 -0
- data/spec/unit/embeddable_spec.rb +498 -0
- data/spec/unit/inherited_spec.rb +33 -0
- data/spec/unit/persistence_spec.rb +481 -0
- data/spec/unit/property_protection_spec.rb +192 -0
- data/spec/unit/property_spec.rb +481 -0
- data/spec/unit/proxyable_spec.rb +376 -0
- data/spec/unit/subclass_spec.rb +85 -0
- data/spec/unit/typecast_spec.rb +521 -0
- data/spec/unit/validations_spec.rb +140 -0
- data/spec/unit/view_spec.rb +367 -0
- metadata +301 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
module CouchRest
|
|
3
|
+
module Model
|
|
4
|
+
module DesignDoc
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
|
|
9
|
+
def design_doc
|
|
10
|
+
@design_doc ||= if auto_update_design_doc
|
|
11
|
+
::CouchRest::Design.new(default_design_doc)
|
|
12
|
+
else
|
|
13
|
+
stored_design_doc || ::CouchRest::Design.new(default_design_doc)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def design_doc_id
|
|
18
|
+
"_design/#{design_doc_slug}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def design_doc_slug
|
|
22
|
+
self.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def design_doc_uri(db = database)
|
|
26
|
+
"#{db.root}/#{design_doc_id}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Retreive the latest version of the design document directly
|
|
30
|
+
# from the database. This is never cached and will return nil if
|
|
31
|
+
# the design is not present.
|
|
32
|
+
#
|
|
33
|
+
# Use this method if you'd like to compare revisions [_rev] which
|
|
34
|
+
# is not stored in the normal design doc.
|
|
35
|
+
def stored_design_doc(db = database)
|
|
36
|
+
db.get(design_doc_id)
|
|
37
|
+
rescue RestClient::ResourceNotFound
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Save the design doc onto a target database in a thread-safe way,
|
|
42
|
+
# not modifying the model's design_doc
|
|
43
|
+
#
|
|
44
|
+
# See also save_design_doc! to always save the design doc even if there
|
|
45
|
+
# are no changes.
|
|
46
|
+
def save_design_doc(db = database, force = false)
|
|
47
|
+
update_design_doc(db, force)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Force the update of the model's design_doc even if it hasn't changed.
|
|
51
|
+
def save_design_doc!(db = database)
|
|
52
|
+
save_design_doc(db, true)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def design_doc_cache
|
|
58
|
+
Thread.current[:couchrest_design_cache] ||= {}
|
|
59
|
+
end
|
|
60
|
+
def design_doc_cache_checksum(db)
|
|
61
|
+
design_doc_cache[design_doc_uri(db)]
|
|
62
|
+
end
|
|
63
|
+
def set_design_doc_cache_checksum(db, checksum)
|
|
64
|
+
design_doc_cache[design_doc_uri(db)] = checksum
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Writes out a design_doc to a given database if forced
|
|
68
|
+
# or the stored checksum is not the same as the current
|
|
69
|
+
# generated checksum.
|
|
70
|
+
#
|
|
71
|
+
# Returns the original design_doc provided, but does
|
|
72
|
+
# not update it with the revision.
|
|
73
|
+
def update_design_doc(db, force = false)
|
|
74
|
+
return design_doc unless force || auto_update_design_doc
|
|
75
|
+
|
|
76
|
+
# Grab the design doc's checksum
|
|
77
|
+
checksum = design_doc.checksum!
|
|
78
|
+
|
|
79
|
+
# If auto updates enabled, check checksum cache
|
|
80
|
+
return design_doc if auto_update_design_doc && design_doc_cache_checksum(db) == checksum
|
|
81
|
+
|
|
82
|
+
retries = 1
|
|
83
|
+
begin
|
|
84
|
+
# Load up the stored doc (if present), update, and save
|
|
85
|
+
saved = stored_design_doc(db)
|
|
86
|
+
if saved
|
|
87
|
+
if force || saved['couchrest-hash'] != checksum
|
|
88
|
+
saved.merge!(design_doc)
|
|
89
|
+
db.save_doc(saved)
|
|
90
|
+
@design_doc = saved # update memo to point to the document we actually saved
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
design_doc.delete('_rev') # This is a new document and so doesn't have a revision yet
|
|
94
|
+
db.save_doc(design_doc)
|
|
95
|
+
end
|
|
96
|
+
rescue RestClient::Conflict
|
|
97
|
+
# if we get a conflict retry the operation...
|
|
98
|
+
raise if retries < 1
|
|
99
|
+
retries -= 1
|
|
100
|
+
retry
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Ensure checksum cached for next attempt if using auto updates
|
|
104
|
+
set_design_doc_cache_checksum(db, checksum) if auto_update_design_doc
|
|
105
|
+
design_doc
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def default_design_doc
|
|
109
|
+
{
|
|
110
|
+
"_id" => design_doc_id,
|
|
111
|
+
"language" => "javascript",
|
|
112
|
+
"views" => {
|
|
113
|
+
'all' => {
|
|
114
|
+
'map' => "function(doc) {
|
|
115
|
+
if (doc['#{self.model_type_key}'] == '#{self.to_s}') {
|
|
116
|
+
emit(doc['_id'],1);
|
|
117
|
+
}
|
|
118
|
+
}"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
end # module ClassMethods
|
|
125
|
+
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
|
|
2
|
+
#### NOTE Work in progress! Not yet used!
|
|
3
|
+
|
|
4
|
+
module CouchRest
|
|
5
|
+
module Model
|
|
6
|
+
|
|
7
|
+
# A design block in CouchRest Model groups together the functionality of CouchDB's
|
|
8
|
+
# design documents in a simple block definition.
|
|
9
|
+
#
|
|
10
|
+
# class Person < CouchRest::Model::Base
|
|
11
|
+
# property :name
|
|
12
|
+
# timestamps!
|
|
13
|
+
#
|
|
14
|
+
# design do
|
|
15
|
+
# view :by_name
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
module Designs
|
|
20
|
+
extend ActiveSupport::Concern
|
|
21
|
+
|
|
22
|
+
module ClassMethods
|
|
23
|
+
|
|
24
|
+
# Add views and other design document features
|
|
25
|
+
# to the current model.
|
|
26
|
+
def design(*args, &block)
|
|
27
|
+
mapper = DesignMapper.new(self)
|
|
28
|
+
mapper.create_view_method(:all)
|
|
29
|
+
|
|
30
|
+
mapper.instance_eval(&block) if block_given?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Override the default page pagination value:
|
|
34
|
+
#
|
|
35
|
+
# class Person < CouchRest::Model::Base
|
|
36
|
+
# paginates_per 10
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
def paginates_per(val)
|
|
40
|
+
@_default_per_page = val
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The models number of documents to return
|
|
44
|
+
# by default when performing pagination.
|
|
45
|
+
# Returns 25 unless explicitly overridden via <tt>paginates_per</tt>
|
|
46
|
+
def default_per_page
|
|
47
|
+
@_default_per_page || 25
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
class DesignMapper
|
|
54
|
+
|
|
55
|
+
attr_accessor :model
|
|
56
|
+
|
|
57
|
+
def initialize(model)
|
|
58
|
+
self.model = model
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Generate a method that will provide a new View instance when
|
|
62
|
+
# requested. This will also define the view in CouchDB unless
|
|
63
|
+
# auto_update_design_doc is disabled.
|
|
64
|
+
def view(name, opts = {})
|
|
65
|
+
View.create(model, name, opts) if model.auto_update_design_doc
|
|
66
|
+
create_view_method(name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Really simple design function that allows a filter
|
|
70
|
+
# to be added. Filters are simple functions used when listening
|
|
71
|
+
# to the _changes feed.
|
|
72
|
+
#
|
|
73
|
+
# No methods are created here, the design is simply updated.
|
|
74
|
+
# See the CouchDB API for more information on how to use this.
|
|
75
|
+
def filter(name, function)
|
|
76
|
+
filters = (self.model.design_doc['filters'] ||= {})
|
|
77
|
+
filters[name.to_s] = function
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def create_view_method(name)
|
|
81
|
+
model.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
|
82
|
+
def self.#{name}(opts = {})
|
|
83
|
+
CouchRest::Model::Designs::View.new(self, opts, '#{name}')
|
|
84
|
+
end
|
|
85
|
+
EOS
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
module CouchRest
|
|
2
|
+
module Model
|
|
3
|
+
module Designs
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# A proxy class that allows view queries to be created using
|
|
7
|
+
# chained method calls. After each call a new instance of the method
|
|
8
|
+
# is created based on the original in a similar fashion to ruby's Sequel
|
|
9
|
+
# library, or Rails 3's Arel.
|
|
10
|
+
#
|
|
11
|
+
# CouchDB views have inherent limitations, so joins and filters as used in
|
|
12
|
+
# a normal relational database are not possible.
|
|
13
|
+
#
|
|
14
|
+
class View
|
|
15
|
+
include Enumerable
|
|
16
|
+
|
|
17
|
+
attr_accessor :owner, :model, :name, :query, :result
|
|
18
|
+
|
|
19
|
+
# Initialize a new View object. This method should not be called from
|
|
20
|
+
# outside CouchRest Model.
|
|
21
|
+
def initialize(parent, new_query = {}, name = nil)
|
|
22
|
+
if parent.is_a?(Class) && parent < CouchRest::Model::Base
|
|
23
|
+
raise "Name must be provided for view to be initialized" if name.nil?
|
|
24
|
+
self.model = parent
|
|
25
|
+
self.owner = parent
|
|
26
|
+
self.name = name.to_s
|
|
27
|
+
# Default options:
|
|
28
|
+
self.query = { }
|
|
29
|
+
elsif parent.is_a?(self.class)
|
|
30
|
+
self.model = (new_query.delete(:proxy) || parent.model)
|
|
31
|
+
self.owner = parent.owner
|
|
32
|
+
self.name = parent.name
|
|
33
|
+
self.query = parent.query.dup
|
|
34
|
+
else
|
|
35
|
+
raise "View cannot be initialized without a parent Model or View"
|
|
36
|
+
end
|
|
37
|
+
query.update(new_query)
|
|
38
|
+
super()
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# == View Execution Methods
|
|
43
|
+
#
|
|
44
|
+
# Request to the CouchDB database using the current query values.
|
|
45
|
+
|
|
46
|
+
# Return each row wrapped in a ViewRow object. Unlike the raw
|
|
47
|
+
# CouchDB request, this will provide an empty array if there
|
|
48
|
+
# are no results.
|
|
49
|
+
def rows
|
|
50
|
+
return @rows if @rows
|
|
51
|
+
if execute && result['rows']
|
|
52
|
+
@rows ||= result['rows'].map{|v| ViewRow.new(v, model)}
|
|
53
|
+
else
|
|
54
|
+
[ ]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Fetch all the documents the view can access. If the view has
|
|
59
|
+
# not already been prepared for including documents in the query,
|
|
60
|
+
# it will be added automatically and reset any previously cached
|
|
61
|
+
# results.
|
|
62
|
+
def all
|
|
63
|
+
include_docs!
|
|
64
|
+
docs
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Provide all the documents from the view. If the view has not been
|
|
68
|
+
# prepared with the +include_docs+ option, each document will be
|
|
69
|
+
# loaded individually.
|
|
70
|
+
def docs
|
|
71
|
+
@docs ||= rows.map{|r| r.doc}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# If another request has been made on the view, this will return
|
|
75
|
+
# the first document in the set. If not, a new query object will be
|
|
76
|
+
# generated with a limit of 1 so that only the first document is
|
|
77
|
+
# loaded.
|
|
78
|
+
def first
|
|
79
|
+
result ? all.first : limit(1).all.first
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Same as first but will order the view in descending order. This
|
|
83
|
+
# does not however reverse the search keys or the offset, so if you
|
|
84
|
+
# are using a +startkey+ and +endkey+ you might end up with
|
|
85
|
+
# unexpected results.
|
|
86
|
+
#
|
|
87
|
+
# If in doubt, don't use this method!
|
|
88
|
+
#
|
|
89
|
+
def last
|
|
90
|
+
result ? all.last : limit(1).descending.all.last
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Return the number of documents in the currently defined result set.
|
|
94
|
+
# Use <tt>#count</tt> for the total number of documents regardless
|
|
95
|
+
# of the current limit defined.
|
|
96
|
+
def length
|
|
97
|
+
docs.length
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Perform a count operation based on the current view. If the view
|
|
101
|
+
# can be reduced, the reduce will be performed and return the first
|
|
102
|
+
# value. This is okay for most simple queries, but may provide
|
|
103
|
+
# unexpected results if your reduce method does not calculate
|
|
104
|
+
# the total number of documents in a result set.
|
|
105
|
+
#
|
|
106
|
+
# Trying to use this method with the group option will raise an error.
|
|
107
|
+
#
|
|
108
|
+
# If no reduce function is defined, a query will be performed
|
|
109
|
+
# to return the total number of rows, this is the equivalant of:
|
|
110
|
+
#
|
|
111
|
+
# view.limit(0).total_rows
|
|
112
|
+
#
|
|
113
|
+
def count
|
|
114
|
+
raise "View#count cannot be used with group options" if query[:group]
|
|
115
|
+
if can_reduce?
|
|
116
|
+
row = reduce.skip(0).limit(1).rows.first
|
|
117
|
+
row.nil? ? 0 : row.value
|
|
118
|
+
else
|
|
119
|
+
limit(0).total_rows
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Check to see if the array of documents is empty. This *will*
|
|
124
|
+
# perform the query and return all documents ready to use, if you don't
|
|
125
|
+
# want to load anything, use +#total_rows+ or +#count+ instead.
|
|
126
|
+
def empty?
|
|
127
|
+
all.empty?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Run through each document provided by the +#all+ method.
|
|
131
|
+
# This is also used by the Enumerator mixin to provide all the standard
|
|
132
|
+
# ruby collection directly on the view.
|
|
133
|
+
def each(&block)
|
|
134
|
+
all.each(&block)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Wrapper for the results offset. As per the CouchDB API,
|
|
138
|
+
# this may be nil if groups are used.
|
|
139
|
+
def offset
|
|
140
|
+
execute['offset']
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Wrapper for the total_rows value provided by the query. As per the
|
|
144
|
+
# CouchDB API, this may be nil if groups are used.
|
|
145
|
+
def total_rows
|
|
146
|
+
execute['total_rows']
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Convenience wrapper to provide all the values from the route
|
|
150
|
+
# set without having to go through +rows+.
|
|
151
|
+
def values
|
|
152
|
+
rows.map{|r| r.value}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Accept requests as if the view was an array. Used for backwards compatibity
|
|
156
|
+
# with older queries:
|
|
157
|
+
#
|
|
158
|
+
# Model.all(:raw => true, :limit => 0)['total_rows']
|
|
159
|
+
#
|
|
160
|
+
# In this example, the raw option will be ignored, and the total rows
|
|
161
|
+
# will still be accessible.
|
|
162
|
+
#
|
|
163
|
+
def [](value)
|
|
164
|
+
execute[value]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# No yet implemented. Eventually this will provide a raw hash
|
|
168
|
+
# of the information CouchDB holds about the view.
|
|
169
|
+
def info
|
|
170
|
+
raise "Not yet implemented"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# == View Filter Methods
|
|
175
|
+
#
|
|
176
|
+
# View filters return a copy of the view instance with the query
|
|
177
|
+
# modified appropriatly. Errors will be raised if the methods
|
|
178
|
+
# are combined in an incorrect fashion.
|
|
179
|
+
#
|
|
180
|
+
|
|
181
|
+
# Find all entries in the index whose key matches the value provided.
|
|
182
|
+
#
|
|
183
|
+
# Cannot be used when the +#startkey+ or +#endkey+ have been set.
|
|
184
|
+
def key(value)
|
|
185
|
+
raise "View#key cannot be used when startkey or endkey have been set" unless query[:keys].nil? && query[:startkey].nil? && query[:endkey].nil?
|
|
186
|
+
update_query(:key => value)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Find all index keys that start with the value provided. May or may
|
|
190
|
+
# not be used in conjunction with the +endkey+ option.
|
|
191
|
+
#
|
|
192
|
+
# When the +#descending+ option is used (not the default), the start
|
|
193
|
+
# and end keys should be reversed, as per the CouchDB API.
|
|
194
|
+
#
|
|
195
|
+
# Cannot be used if the key has been set.
|
|
196
|
+
def startkey(value)
|
|
197
|
+
raise "View#startkey cannot be used when key has been set" unless query[:key].nil? && query[:keys].nil?
|
|
198
|
+
update_query(:startkey => value)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The result set should start from the position of the provided document.
|
|
202
|
+
# The value may be provided as an object that responds to the +#id+ call
|
|
203
|
+
# or a string.
|
|
204
|
+
def startkey_doc(value)
|
|
205
|
+
update_query(:startkey_docid => value.is_a?(String) ? value : value.id)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# The opposite of +#startkey+, finds all index entries whose key is before
|
|
209
|
+
# the value specified.
|
|
210
|
+
#
|
|
211
|
+
# See the +#startkey+ method for more details and the +#inclusive_end+
|
|
212
|
+
# option.
|
|
213
|
+
def endkey(value)
|
|
214
|
+
raise "View#endkey cannot be used when key has been set" unless query[:key].nil? && query[:keys].nil?
|
|
215
|
+
update_query(:endkey => value)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# The result set should end at the position of the provided document.
|
|
219
|
+
# The value may be provided as an object that responds to the +#id+
|
|
220
|
+
# call or a string.
|
|
221
|
+
def endkey_doc(value)
|
|
222
|
+
update_query(:endkey_docid => value.is_a?(String) ? value : value.id)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Keys is a special CouchDB option that will cause the view request to be POSTed
|
|
226
|
+
# including an array of keys. Only documents with the matching keys will be
|
|
227
|
+
# returned. This is much faster than sending multiple requests for a set
|
|
228
|
+
# non-consecutive documents.
|
|
229
|
+
#
|
|
230
|
+
# If no values are provided, this method will act as a wrapper around
|
|
231
|
+
# the rows result set, providing an array of keys.
|
|
232
|
+
def keys(*keys)
|
|
233
|
+
if keys.empty?
|
|
234
|
+
rows.map{|r| r.key}
|
|
235
|
+
else
|
|
236
|
+
raise "View#keys cannot by used when key or startkey/endkey have been set" unless query[:key].nil? && query[:startkey].nil? && query[:endkey].nil?
|
|
237
|
+
update_query(:keys => keys.first)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# The results should be provided in descending order. If the startkey or
|
|
243
|
+
# endkey query options have already been seen set, calling this method
|
|
244
|
+
# will automatically swap the options around. If you don't want this,
|
|
245
|
+
# simply set descending before any other option.
|
|
246
|
+
#
|
|
247
|
+
# Descending is false by default, and this method cannot
|
|
248
|
+
# be undone once used, it has no inverse option.
|
|
249
|
+
def descending
|
|
250
|
+
if query[:startkey] || query[:endkey]
|
|
251
|
+
query[:startkey], query[:endkey] = query[:endkey], query[:startkey]
|
|
252
|
+
elsif query[:startkey_docid] || query[:endkey_docid]
|
|
253
|
+
query[:startkey_docid], query[:endkey_docid] = query[:endkey_docid], query[:startkey_docid]
|
|
254
|
+
end
|
|
255
|
+
update_query(:descending => true)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Limit the result set to the value supplied.
|
|
259
|
+
def limit(value)
|
|
260
|
+
update_query(:limit => value)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Skip the number of entries in the index specified by value. This would be
|
|
264
|
+
# the equivilent of an offset in SQL.
|
|
265
|
+
#
|
|
266
|
+
# The CouchDB documentation states that the skip option should not be used
|
|
267
|
+
# with large data sets as it is inefficient. Use the +startkey_doc+ method
|
|
268
|
+
# instead to skip ranges efficiently.
|
|
269
|
+
def skip(value = 0)
|
|
270
|
+
update_query(:skip => value)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Use the reduce function on the view. If none is available this method
|
|
274
|
+
# will fail.
|
|
275
|
+
def reduce
|
|
276
|
+
raise "Cannot reduce a view without a reduce method" unless can_reduce?
|
|
277
|
+
update_query(:reduce => true, :include_docs => nil)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Control whether the reduce function reduces to a set of distinct keys
|
|
281
|
+
# or to a single result row.
|
|
282
|
+
#
|
|
283
|
+
# By default the value is false, and can only be set when the view's
|
|
284
|
+
# +#reduce+ option has been set.
|
|
285
|
+
def group
|
|
286
|
+
raise "View#reduce must have been set before grouping is permitted" unless query[:reduce]
|
|
287
|
+
update_query(:group => true)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Will set the level the grouping should be performed to. As per the
|
|
291
|
+
# CouchDB API, it only makes sense when the index key is an array.
|
|
292
|
+
#
|
|
293
|
+
# This will automatically set the group option.
|
|
294
|
+
def group_level(value)
|
|
295
|
+
group.update_query(:group_level => value.to_i)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def include_docs
|
|
299
|
+
update_query.include_docs!
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
### Special View Filter Methods
|
|
303
|
+
|
|
304
|
+
# Specify the database the view should use. If not defined,
|
|
305
|
+
# an attempt will be made to load its value from the model.
|
|
306
|
+
def database(value)
|
|
307
|
+
update_query(:database => value)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Set the view's proxy that will be used instead of the model
|
|
311
|
+
# for any future searches. As soon as this enters the
|
|
312
|
+
# new object's initializer it will be removed and replace
|
|
313
|
+
# the model object.
|
|
314
|
+
#
|
|
315
|
+
# See the Proxyable mixin for more details.
|
|
316
|
+
#
|
|
317
|
+
def proxy(value)
|
|
318
|
+
update_query(:proxy => value)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Return any cached values to their nil state so that any queries
|
|
322
|
+
# requested later will have a fresh set of data.
|
|
323
|
+
def reset!
|
|
324
|
+
self.result = nil
|
|
325
|
+
@rows = nil
|
|
326
|
+
@docs = nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# == Kaminari compatible pagination support
|
|
330
|
+
#
|
|
331
|
+
# Based on the really simple support for scoped pagination in the
|
|
332
|
+
# the Kaminari gem, we provide compatible methods here to perform
|
|
333
|
+
# the same actions you'd expect.
|
|
334
|
+
#
|
|
335
|
+
|
|
336
|
+
def page(page)
|
|
337
|
+
limit(owner.default_per_page).skip(owner.default_per_page * ([page.to_i, 1].max - 1))
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def per(num)
|
|
341
|
+
raise "View#page must be called before #per!" if limit_value.nil? || offset_value.nil?
|
|
342
|
+
if (n = num.to_i) <= 0
|
|
343
|
+
self
|
|
344
|
+
else
|
|
345
|
+
limit(num).skip(offset_value / limit_value * n)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def total_count
|
|
350
|
+
@total_count ||= limit(nil).skip(nil).count
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def offset_value
|
|
354
|
+
query[:skip]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def limit_value
|
|
358
|
+
query[:limit]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def num_pages
|
|
362
|
+
(total_count.to_f / limit_value).ceil
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def current_page
|
|
366
|
+
(offset_value / limit_value) + 1
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
protected
|
|
370
|
+
|
|
371
|
+
def include_docs!
|
|
372
|
+
raise "Cannot include documents in view that has been reduced!" if query[:reduce]
|
|
373
|
+
reset! if result && !include_docs?
|
|
374
|
+
query[:include_docs] = true
|
|
375
|
+
self
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def include_docs?
|
|
379
|
+
!!query[:include_docs]
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def update_query(new_query = {})
|
|
383
|
+
self.class.new(self, new_query)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def design_doc
|
|
387
|
+
model.design_doc
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def can_reduce?
|
|
391
|
+
!design_doc['views'][name]['reduce'].blank?
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def use_database
|
|
395
|
+
query[:database] || model.database
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def execute
|
|
399
|
+
return self.result if result
|
|
400
|
+
raise "Database must be defined in model or view!" if use_database.nil?
|
|
401
|
+
|
|
402
|
+
# Remove the reduce value if its not needed to prevent CouchDB errors
|
|
403
|
+
query.delete(:reduce) unless can_reduce?
|
|
404
|
+
|
|
405
|
+
model.save_design_doc(use_database)
|
|
406
|
+
|
|
407
|
+
self.result = model.design_doc.view_on(use_database, name, query.reject{|k,v| v.nil?})
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Class Methods
|
|
411
|
+
class << self
|
|
412
|
+
# Simplified view creation. A new view will be added to the
|
|
413
|
+
# provided model's design document using the name and options.
|
|
414
|
+
#
|
|
415
|
+
# If the view name starts with "by_" and +:by+ is not provided in
|
|
416
|
+
# the options, the new view's map method will be interpreted and
|
|
417
|
+
# generated automatically. For example:
|
|
418
|
+
#
|
|
419
|
+
# View.create(Meeting, "by_date_and_name")
|
|
420
|
+
#
|
|
421
|
+
# Will create a view that searches by the date and name properties.
|
|
422
|
+
# Explicity setting the attributes to use is possible using the
|
|
423
|
+
# +:by+ option. For example:
|
|
424
|
+
#
|
|
425
|
+
# View.create(Meeting, "by_date_and_name", :by => [:date, :firstname, :lastname])
|
|
426
|
+
#
|
|
427
|
+
# The view name is the same, but three keys would be used in the
|
|
428
|
+
# subsecuent index.
|
|
429
|
+
#
|
|
430
|
+
# By default, a check is made on each of the view's keys to ensure they
|
|
431
|
+
# do not contain a nil value ('null' in javascript). This is probably what
|
|
432
|
+
# you want in most cases but sometimes in can be useful to create an
|
|
433
|
+
# index where nil is permited. Set the <tt>:allow_nil</tt> option to true to
|
|
434
|
+
# remove this check.
|
|
435
|
+
#
|
|
436
|
+
# Conversely, keys are not checked to see if they are empty or blank. If you'd
|
|
437
|
+
# like to enable this, set the <tt>:allow_blank</tt> option to false. The default
|
|
438
|
+
# is true, empty strings are permited in the indexes.
|
|
439
|
+
#
|
|
440
|
+
def create(model, name, opts = {})
|
|
441
|
+
|
|
442
|
+
unless opts[:map]
|
|
443
|
+
if opts[:by].nil? && name.to_s =~ /^by_(.+)/
|
|
444
|
+
opts[:by] = $1.split(/_and_/)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
|
|
448
|
+
|
|
449
|
+
opts[:allow_blank] = opts[:allow_blank].nil? ? true : opts[:allow_blank]
|
|
450
|
+
opts[:guards] ||= []
|
|
451
|
+
opts[:guards].push "(doc['#{model.model_type_key}'] == '#{model.to_s}')"
|
|
452
|
+
|
|
453
|
+
keys = opts[:by].map{|o| "doc['#{o}']"}
|
|
454
|
+
emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]"
|
|
455
|
+
opts[:guards] += keys.map{|k| "(#{k} != null)"} unless opts[:allow_nil]
|
|
456
|
+
opts[:guards] += keys.map{|k| "(#{k} != '')"} unless opts[:allow_blank]
|
|
457
|
+
opts[:map] = <<-EOF
|
|
458
|
+
function(doc) {
|
|
459
|
+
if (#{opts[:guards].join(' && ')}) {
|
|
460
|
+
emit(#{emit}, 1);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
EOF
|
|
464
|
+
opts[:reduce] = <<-EOF
|
|
465
|
+
function(key, values, rereduce) {
|
|
466
|
+
return sum(values);
|
|
467
|
+
}
|
|
468
|
+
EOF
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
model.design_doc['views'] ||= {}
|
|
472
|
+
view = model.design_doc['views'][name.to_s] = { }
|
|
473
|
+
view['map'] = opts[:map]
|
|
474
|
+
view['reduce'] = opts[:reduce] if opts[:reduce]
|
|
475
|
+
view
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# A special wrapper class that provides easy access to the key
|
|
483
|
+
# fields in a result row.
|
|
484
|
+
class ViewRow < Hash
|
|
485
|
+
attr_reader :model
|
|
486
|
+
def initialize(hash, model)
|
|
487
|
+
@model = model
|
|
488
|
+
replace(hash)
|
|
489
|
+
end
|
|
490
|
+
def id
|
|
491
|
+
self["id"]
|
|
492
|
+
end
|
|
493
|
+
def key
|
|
494
|
+
self["key"]
|
|
495
|
+
end
|
|
496
|
+
def value
|
|
497
|
+
self['value']
|
|
498
|
+
end
|
|
499
|
+
def raw_doc
|
|
500
|
+
self['doc']
|
|
501
|
+
end
|
|
502
|
+
# Send a request for the linked document either using the "id" field's
|
|
503
|
+
# value, or the ["value"]["_id"] used for linked documents.
|
|
504
|
+
def doc
|
|
505
|
+
return model.build_from_database(self['doc']) if self['doc']
|
|
506
|
+
doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id
|
|
507
|
+
doc_id ? model.get(doc_id) : nil
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
end
|