Adding support for proxying and more refinements to views
This commit is contained in:
parent
63bb1bb6bd
commit
a78e3b74d6
14 changed files with 560 additions and 46 deletions
|
@ -109,7 +109,7 @@ module CouchRest
|
|||
base = options[:proxy] || options[:class_name]
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{attrib}
|
||||
@#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : #{base}.get(self.#{options[:foreign_key]})
|
||||
@#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : (model_proxy || #{base}).get(self.#{options[:foreign_key]})
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
@ -140,7 +140,7 @@ module CouchRest
|
|||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{attrib}(reload = false)
|
||||
return @#{attrib} unless @#{attrib}.nil? or reload
|
||||
ary = self.#{options[:foreign_key]}.collect{|i| #{base}.get(i)}
|
||||
ary = self.#{options[:foreign_key]}.collect{|i| (model_proxy || #{base}).get(i)}
|
||||
@#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
|
||||
end
|
||||
EOS
|
||||
|
|
|
@ -12,6 +12,7 @@ module CouchRest
|
|||
include CouchRest::Model::DesignDoc
|
||||
include CouchRest::Model::ExtendedAttachments
|
||||
include CouchRest::Model::ClassProxy
|
||||
include CouchRest::Model::Proxyable
|
||||
include CouchRest::Model::Collection
|
||||
include CouchRest::Model::PropertyProtection
|
||||
include CouchRest::Model::Associations
|
||||
|
@ -46,9 +47,12 @@ module CouchRest
|
|||
# Options supported:
|
||||
#
|
||||
# * :directly_set_attributes: true when data comes directly from database
|
||||
# * :database: provide an alternative database
|
||||
#
|
||||
def initialize(doc = {}, options = {})
|
||||
doc = prepare_all_attributes(doc, options)
|
||||
# set the instances database, if provided
|
||||
self.database = options[:database] unless options[:database].nil?
|
||||
super(doc)
|
||||
unless self['_id'] && self['_rev']
|
||||
self[self.model_type_key] = self.class.to_s
|
||||
|
|
|
@ -222,7 +222,7 @@ module CouchRest
|
|||
if @container_class.nil?
|
||||
results
|
||||
else
|
||||
results['rows'].collect { |row| @container_class.create_from_database(row['doc']) } unless results['rows'].nil?
|
||||
results['rows'].collect { |row| @container_class.build_from_database(row['doc']) } unless results['rows'].nil?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,18 +5,19 @@ module CouchRest
|
|||
#
|
||||
# A proxy class that allows view queries to be created using
|
||||
# chained method calls. After each call a new instance of the method
|
||||
# is created based on the original in a similar fashion to ruby's sequel
|
||||
# is created based on the original in a similar fashion to ruby's Sequel
|
||||
# library, or Rails 3's Arel.
|
||||
#
|
||||
# CouchDB views have inherent limitations, so joins and filters as used in
|
||||
# a normal relational database are not possible. At least not yet!
|
||||
# a normal relational database are not possible.
|
||||
#
|
||||
class View
|
||||
include Enumerable
|
||||
|
||||
attr_accessor :model, :name, :query, :result
|
||||
|
||||
# Initialize a new View object. This method should not be called from outside CouchRest Model.
|
||||
# Initialize a new View object. This method should not be called from
|
||||
# outside CouchRest Model.
|
||||
def initialize(parent, new_query = {}, name = nil)
|
||||
if parent.is_a?(Class) && parent < CouchRest::Model::Base
|
||||
raise "Name must be provided for view to be initialized" if name.nil?
|
||||
|
@ -25,14 +26,14 @@ module CouchRest
|
|||
# Default options:
|
||||
self.query = { :reduce => false }
|
||||
elsif parent.is_a?(self.class)
|
||||
self.model = parent.model
|
||||
self.model = (new_query.delete(:proxy) || parent.model)
|
||||
self.name = parent.name
|
||||
self.query = parent.query.dup
|
||||
else
|
||||
raise "View cannot be initialized without a parent Model or View"
|
||||
end
|
||||
query.update(new_query)
|
||||
super
|
||||
super()
|
||||
end
|
||||
|
||||
|
||||
|
@ -110,6 +111,13 @@ module CouchRest
|
|||
end
|
||||
end
|
||||
|
||||
# Check to see if the array of documents is empty. This *will*
|
||||
# perform the query and return all documents ready to use, if you don't
|
||||
# want to load anything, use +#total_rows+ or +#count+ instead.
|
||||
def empty?
|
||||
all.empty?
|
||||
end
|
||||
|
||||
# Run through each document provided by the +#all+ method.
|
||||
# This is also used by the Enumerator mixin to provide all the standard
|
||||
# ruby collection directly on the view.
|
||||
|
@ -141,6 +149,18 @@ module CouchRest
|
|||
rows.map{|r| r.value}
|
||||
end
|
||||
|
||||
# Accept requests as if the view was an array. Used for backwards compatibity
|
||||
# with older queries:
|
||||
#
|
||||
# Model.all(:raw => true, :limit => 0)['total_rows']
|
||||
#
|
||||
# In this example, the raw option will be ignored, and the total rows
|
||||
# will still be accessible.
|
||||
#
|
||||
def [](value)
|
||||
execute[value]
|
||||
end
|
||||
|
||||
# No yet implemented. Eventually this will provide a raw hash
|
||||
# of the information CouchDB holds about the view.
|
||||
def info
|
||||
|
@ -150,16 +170,11 @@ module CouchRest
|
|||
|
||||
# == View Filter Methods
|
||||
#
|
||||
# View filters return an copy of the view instance with the query
|
||||
# View filters return a copy of the view instance with the query
|
||||
# modified appropriatly. Errors will be raised if the methods
|
||||
# are combined in an incorrect fashion.
|
||||
#
|
||||
|
||||
# Specify the database the view should use. If not defined,
|
||||
# an attempt will be made to load its value from the model.
|
||||
def database(value)
|
||||
update_query(:database => value)
|
||||
end
|
||||
|
||||
# Find all entries in the index whose key matches the value provided.
|
||||
#
|
||||
|
@ -229,7 +244,8 @@ module CouchRest
|
|||
update_query(:skip => value)
|
||||
end
|
||||
|
||||
# Use the reduce function on the view. If none is available this method will fail.
|
||||
# Use the reduce function on the view. If none is available this method
|
||||
# will fail.
|
||||
def reduce
|
||||
raise "Cannot reduce a view without a reduce method" unless can_reduce?
|
||||
update_query(:reduce => true)
|
||||
|
@ -257,6 +273,25 @@ module CouchRest
|
|||
update_query.include_docs!
|
||||
end
|
||||
|
||||
### Special View Filter Methods
|
||||
|
||||
# Specify the database the view should use. If not defined,
|
||||
# an attempt will be made to load its value from the model.
|
||||
def database(value)
|
||||
update_query(:database => value)
|
||||
end
|
||||
|
||||
# Set the view's proxy that will be used instead of the model
|
||||
# for any future searches. As soon as this enters the
|
||||
# new object's initializer it will be removed and replace
|
||||
# the model object.
|
||||
#
|
||||
# See the Proxyable mixin for more details.
|
||||
#
|
||||
def proxy(value)
|
||||
update_query(:proxy => value)
|
||||
end
|
||||
|
||||
# Return any cached values to their nil state so that any queries
|
||||
# requested later will have a fresh set of data.
|
||||
def reset!
|
||||
|
@ -288,20 +323,22 @@ module CouchRest
|
|||
def can_reduce?
|
||||
!design_doc['views'][name]['reduce'].blank?
|
||||
end
|
||||
|
||||
|
||||
def use_database
|
||||
query[:database] || model.database
|
||||
end
|
||||
|
||||
def execute
|
||||
return self.result if result
|
||||
db = query[:database] || model.database
|
||||
raise "Database must be defined in model or view!" if db.nil?
|
||||
raise "Database must be defined in model or view!" if use_database.nil?
|
||||
retryable = true
|
||||
# Remove the reduce value if its not needed
|
||||
query.delete(:reduce) unless can_reduce?
|
||||
begin
|
||||
self.result = model.design_doc.view_on(db, name, query)
|
||||
self.result = model.design_doc.view_on(use_database, name, query)
|
||||
rescue RestClient::ResourceNotFound => e
|
||||
if retryable
|
||||
model.save_design_doc(db)
|
||||
model.save_design_doc(use_database)
|
||||
retryable = false
|
||||
retry
|
||||
else
|
||||
|
@ -334,9 +371,10 @@ module CouchRest
|
|||
def create(model, name, opts = {})
|
||||
|
||||
unless opts[:map]
|
||||
if opts[:by].nil? && name =~ /^by_(.+)/
|
||||
if opts[:by].nil? && name.to_s =~ /^by_(.+)/
|
||||
opts[:by] = $1.split(/_and_/)
|
||||
end
|
||||
|
||||
raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
|
||||
|
||||
opts[:guards] ||= []
|
||||
|
@ -373,9 +411,9 @@ module CouchRest
|
|||
# A special wrapper class that provides easy access to the key
|
||||
# fields in a result row.
|
||||
class ViewRow < Hash
|
||||
attr_accessor :model
|
||||
attr_reader :model
|
||||
def initialize(hash, model)
|
||||
self.model = model
|
||||
@model = model
|
||||
replace(hash)
|
||||
end
|
||||
def id
|
||||
|
@ -393,7 +431,7 @@ module CouchRest
|
|||
# Send a request for the linked document either using the "id" field's
|
||||
# value, or the ["value"]["_id"] used for linked documents.
|
||||
def doc
|
||||
return model.create_from_database(self['doc']) if self['doc']
|
||||
return model.build_from_database(self['doc']) if self['doc']
|
||||
doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id
|
||||
model.get(doc_id)
|
||||
end
|
||||
|
|
|
@ -88,7 +88,7 @@ module CouchRest
|
|||
def get!(id, db = database)
|
||||
raise "Missing or empty document ID" if id.to_s.empty?
|
||||
doc = db.get id
|
||||
create_from_database(doc)
|
||||
build_from_database(doc)
|
||||
end
|
||||
alias :find! :get!
|
||||
|
||||
|
|
|
@ -93,12 +93,12 @@ module CouchRest
|
|||
|
||||
# Creates a new instance, bypassing attribute protection
|
||||
#
|
||||
#
|
||||
# ==== Returns
|
||||
# a document instance
|
||||
def create_from_database(doc = {})
|
||||
#
|
||||
def build_from_database(doc = {})
|
||||
base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
|
||||
base.new(doc, :directly_set_attributes => true)
|
||||
base.new(doc, :directly_set_attributes => true)
|
||||
end
|
||||
|
||||
# Defines an instance and save it directly to the database
|
||||
|
|
152
lib/couchrest/model/proxyable.rb
Normal file
152
lib/couchrest/model/proxyable.rb
Normal file
|
@ -0,0 +1,152 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
# :nodoc: Because I like inventing words
|
||||
module Proxyable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
attr_accessor :model_proxy
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
# Define a collection that will use the base model for the database connection
|
||||
# details.
|
||||
def proxy_for(model_name, options = {})
|
||||
db_method = options[:database_method] || "proxy_database"
|
||||
options[:class_name] ||= model_name.to_s.singularize.camelize
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{model_name}
|
||||
unless respond_to?('#{db_method}')
|
||||
raise "Missing ##{db_method} method for proxy"
|
||||
end
|
||||
@#{model_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(#{options[:class_name]}, self, '#{model_name}', #{db_method})
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
def proxied_by(model_name, options = {})
|
||||
raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name)
|
||||
attr_accessor model_name
|
||||
end
|
||||
end
|
||||
|
||||
class ModelProxy
|
||||
|
||||
attr_reader :model, :owner, :owner_name, :database
|
||||
|
||||
def initialize(model, owner, owner_name, database)
|
||||
@model = model
|
||||
@owner = owner
|
||||
@owner_name = owner_name
|
||||
@database = database
|
||||
end
|
||||
|
||||
# Base
|
||||
|
||||
def new(*args)
|
||||
proxy_update(model.new(*args))
|
||||
end
|
||||
|
||||
def build_from_database(doc = {})
|
||||
proxy_update(model.build_from_database(doc))
|
||||
end
|
||||
|
||||
def method_missing(m, *args, &block)
|
||||
if has_view?(m)
|
||||
if model.respond_to?(m)
|
||||
return model.send(m, *args).proxy(self)
|
||||
else
|
||||
query = args.shift || {}
|
||||
return view(m, query, *args, &block)
|
||||
end
|
||||
elsif m.to_s =~ /^find_(by_.+)/
|
||||
view_name = $1
|
||||
if has_view?(view_name)
|
||||
return first_from_view(view_name, *args)
|
||||
end
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# DocumentQueries
|
||||
|
||||
def all(opts = {}, &block)
|
||||
proxy_update_all(@model.all({:database => @database}.merge(opts), &block))
|
||||
end
|
||||
|
||||
def count(opts = {})
|
||||
@model.count({:database => @database}.merge(opts))
|
||||
end
|
||||
|
||||
def first(opts = {})
|
||||
proxy_update(@model.first({:database => @database}.merge(opts)))
|
||||
end
|
||||
|
||||
def last(opts = {})
|
||||
proxy_update(@model.last({:database => @database}.merge(opts)))
|
||||
end
|
||||
|
||||
def get(id)
|
||||
proxy_update(@model.get(id, @database))
|
||||
end
|
||||
alias :find :get
|
||||
|
||||
# Views
|
||||
|
||||
def has_view?(view)
|
||||
@model.has_view?(view)
|
||||
end
|
||||
|
||||
def view_by(*args)
|
||||
@model.view_by(*args)
|
||||
end
|
||||
|
||||
def view(name, query={}, &block)
|
||||
proxy_update_all(@model.view(name, {:database => @database}.merge(query), &block))
|
||||
end
|
||||
|
||||
def first_from_view(name, *args)
|
||||
# add to first hash available, or add to end
|
||||
(args.last.is_a?(Hash) ? args.last : (args << {}).last)[:database] = @database
|
||||
proxy_update(@model.first_from_view(name, *args))
|
||||
end
|
||||
|
||||
# DesignDoc
|
||||
|
||||
def design_doc
|
||||
@model.design_doc
|
||||
end
|
||||
|
||||
def refresh_design_doc
|
||||
@model.refresh_design_doc(@database)
|
||||
end
|
||||
|
||||
def save_design_doc
|
||||
@model.save_design_doc(@database)
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
|
||||
# Update the document's proxy details, specifically, the fields that
|
||||
# link back to the original document.
|
||||
def proxy_update(doc)
|
||||
if doc
|
||||
doc.database = @database if doc.respond_to?(:database=)
|
||||
doc.model_proxy = self if doc.respond_to?(:model_proxy=)
|
||||
doc.send("#{owner_name}=", owner) if doc.respond_to?("#{owner_name}=")
|
||||
end
|
||||
doc
|
||||
end
|
||||
|
||||
def proxy_update_all(docs)
|
||||
docs.each do |doc|
|
||||
proxy_update(doc)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,19 +9,19 @@ module CouchRest
|
|||
|
||||
# Ensure we have a class available so we can check for a usable view
|
||||
# or add one if necessary.
|
||||
def setup(klass)
|
||||
@klass = klass
|
||||
def setup(model)
|
||||
@model = model
|
||||
end
|
||||
|
||||
|
||||
def validate_each(document, attribute, value)
|
||||
view_name = options[:view].nil? ? "by_#{attribute}" : options[:view]
|
||||
model = document.model_proxy || @model
|
||||
# Determine the base of the search
|
||||
base = options[:proxy].nil? ? @klass : document.instance_eval(options[:proxy])
|
||||
base = options[:proxy].nil? ? model : document.instance_eval(options[:proxy])
|
||||
|
||||
if base.respond_to?(:has_view?) && !base.has_view?(view_name)
|
||||
raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil?
|
||||
@klass.view_by attribute
|
||||
model.view_by attribute
|
||||
end
|
||||
|
||||
docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows']
|
||||
|
@ -36,7 +36,6 @@ module CouchRest
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -131,7 +131,7 @@ module CouchRest
|
|||
collection_proxy_for(design_doc, name, opts.merge({:database => db, :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']
|
||||
view['rows'].collect{|r|build_from_database(r['doc'])} if view['rows']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue