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