openlogic-couchrest_model 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|