module CouchRest module Mixins module Views def self.included(base) base.extend(ClassMethods) end 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 self.design_doc.view_by(*keys) self.design_doc_fresh = false end # returns stored defaults if the 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) db = query.delete(:database) || database unless design_doc_fresh refresh_design_doc_on(db) end query[:raw] = true if query[:reduce] raw = query.delete(:raw) fetch_view_with_docs(db, name, query, raw, &block) end # DEPRECATED # user model_design_doc to retrieve the current design doc def all_design_doc_versions(db = database) db.documents :startkey => "_design/#{self.to_s}", :endkey => "_design/#{self.to_s}-\u9999" end def model_design_doc(db = database) begin @model_design_doc = db.get("_design/#{self.to_s}") rescue nil end end # Deletes the current design doc for the current class. # Running it to early could mean that live code has to regenerate # potentially large indexes. def cleanup_design_docs!(db = database) save_design_doc_on(db) 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|new(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|new(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 HttpAbstraction::ResourceNotFound => e if retryable save_design_doc_on(db) retryable = false retry else raise e end end end end # module ClassMethods end end end