module CouchRest
module Model
module Views
extend ActiveSupport::Concern
module ClassMethods
# Define a CouchDB view. The name of the view will be the concatenation
# of by and the keys joined by _and_
#
# ==== Example views:
#
# class Post
# # view with default options
# # query with Post.by_date
# view_by :date, :descending => true
#
# # view with compound sort-keys
# # query with Post.by_user_id_and_date
# view_by :user_id, :date
#
# # view with custom map/reduce functions
# # query with Post.by_tags :reduce => true
# view_by :tags,
# :map =>
# "function(doc) {
# if (doc['couchrest-type'] == 'Post' && doc.tags) {
# doc.tags.forEach(function(tag){
# emit(doc.tag, 1);
# });
# }
# }",
# :reduce =>
# "function(keys, values, rereduce) {
# return sum(values);
# }"
# end
#
# view_by :date will create a view defined by this Javascript
# function:
#
# function(doc) {
# if (doc['couchrest-type'] == 'Post' && doc.date) {
# emit(doc.date, null);
# }
# }
#
# It can be queried by calling Post.by_date which accepts all
# valid options for CouchRest::Database#view. In addition, calling with
# the :raw => true option will return the view rows
# themselves. By default Post.by_date will return the
# documents included in the generated view.
#
# Calling with :database => [instance of CouchRest::Database] will
# send the query to a specific database, otherwise it will go to
# the model's default database (use_database)
#
# CouchRest::Database#view options can be applied at view definition
# time as defaults, and they will be curried and used at view query
# time. Or they can be overridden at query time.
#
# Custom views can be queried with :reduce => true to return
# reduce results. The default for custom views is to query with
# :reduce => false.
#
# Views are generated (on a per-model basis) lazily on first-access.
# This means that if you are deploying changes to a view, the views for
# that model won't be available until generation is complete. This can
# take some time with large databases. Strategies are in the works.
#
# To understand the capabilities of this view system more completely,
# it is recommended that you read the RSpec file at
# spec/couchrest/more/extended_doc_spec.rb.
def view_by(*keys)
opts = keys.pop if keys.last.is_a?(Hash)
opts ||= {}
ducktype = opts.delete(:ducktype)
unless ducktype || opts[:map]
opts[:guards] ||= []
opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
end
keys.push opts
design_doc.view_by(*keys)
req_design_doc_refresh
end
# returns stored defaults if there is a view named this in the design doc
def has_view?(view)
view = view.to_s
design_doc && design_doc['views'] && design_doc['views'][view]
end
# Dispatches to any named view.
def view(name, query={}, &block)
query = query.dup # Modifications made on copy!
db = query.delete(:database) || database
refresh_design_doc(db)
query[:raw] = true if query[:reduce]
raw = query.delete(:raw)
fetch_view_with_docs(db, name, query, raw, &block)
end
# Find the first entry in the view. If the second parameter is a string
# it will be used as the key for the request, for example:
#
# Course.first_from_view('by_teacher', 'Fred')
#
# More advanced requests can be performed by providing a hash:
#
# Course.first_from_view('by_teacher', :startkey => 'bbb', :endkey => 'eee')
#
def first_from_view(name, *args)
query = {:limit => 1}
case args.first
when String, Array
query.update(args[1]) unless args[1].nil?
query[:key] = args.first
when Hash
query.update(args.first)
end
view(name, query).first
end
private
def fetch_view_with_docs(db, name, opts, raw=false, &block)
if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
fetch_view(db, name, opts, &block)
else
begin
if block.nil?
collection_proxy_for(design_doc, name, opts.merge({:include_docs => true}))
else
view = fetch_view db, name, opts.merge({:include_docs => true}), &block
view['rows'].collect{|r|create_from_database(r['doc'])} if view['rows']
end
rescue
# fallback for old versions of couchdb that don't
# have include_docs support
view = fetch_view(db, name, opts, &block)
view['rows'].collect{|r|create_from_database(db.get(r['id']))} if view['rows']
end
end
end
def fetch_view(db, view_name, opts, &block)
raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
retryable = true
begin
design_doc.view_on(db, view_name, opts, &block)
# the design doc may not have been saved yet on this database
rescue RestClient::ResourceNotFound => e
if retryable
save_design_doc(db)
retryable = false
retry
else
raise e
end
end
end
end # module ClassMethods
end
end
end