A from-scratch fork of simply_couch's model layer that runs on Couchbase natively: `include SimplyBased::Model` instead of `include SimplyCouch::Model`. - SimplyBased::Store replaces the CouchDB layer: KV documents, N1QL/SQL++ finders + lazily-created GSIs, CAS mapped onto _rev. No design documents or JavaScript map/reduce — the hand-written association views become N1QL (belongs_to/has_many/has_one scalar FK; habtm via ANY ... SATISFIES + array index, or KV multi-get on the storing side). - Redis replacements on the same cluster: Counter (atomic increment/decrement), Cache (ActiveSupport::Cache::Store over KV+TTL, drop-in Rails.cache/ActiveJob), Lock (insert-if-absent + TTL + CAS-checked release). - Runs the simply_couch test suite against live Couchbase: 378/432 applicable examples green (CouchDB-mechanic specs tagged :couchdb_only and excluded). - Remaining: ancestry (tree) + embedded_in reverse-lookups (recursive/embedded N1QL) and a few pagination/lifecycle edges. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
3.1 KiB
Ruby
107 lines
3.1 KiB
Ruby
module SimplyBased
|
|
module Model
|
|
module View
|
|
# A view to return model instances by searching its properties.
|
|
# If you pass reduce => true will count instead
|
|
#
|
|
# example:
|
|
# view :my_view, :key => :name
|
|
#
|
|
# in addition you can pass in conditions as a javascript string
|
|
# view :my_view_only_completed, :key => :name, :conditions => 'doc.completed = true'
|
|
# and also a results filter (the results will be run through the given proc):
|
|
# view :my_view, :key => :name, :results_filter => lambda{|results| results.size}
|
|
class ModelViewSpec < BaseViewSpec
|
|
# The key simply_couch uses for class identification in CouchDB documents
|
|
RUBY_CLASS_KEY = 'ruby_class'
|
|
|
|
class JavascriptGenerator
|
|
def initialize(options, klass)
|
|
@options = options
|
|
@klass = klass
|
|
end
|
|
|
|
def map_body(&block)
|
|
<<-JS
|
|
function(doc) {
|
|
if(doc.#{RUBY_CLASS_KEY} && doc.#{RUBY_CLASS_KEY} == '#{@klass.name}'#{conditions}) {
|
|
#{yield}
|
|
}
|
|
}
|
|
JS
|
|
end
|
|
|
|
def map_function
|
|
map_body do
|
|
"emit(#{formatted_key}, #{emit_value});"
|
|
end
|
|
end
|
|
|
|
def formatted_key(_key = nil)
|
|
_key ||= @options[:key]
|
|
if _key.is_a? Array
|
|
'[' + _key.map{|key_part| formatted_key(key_part)}.join(', ') + ']'
|
|
else
|
|
"doc['#{_key}']"
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# Allow custom emit values. Raise when the specified argument is not recognized
|
|
def emit_value
|
|
case @options[:emit_value]
|
|
when Symbol then "doc['#{@options[:emit_value]}']"
|
|
when String then @options[:emit_value]
|
|
when Numeric then @options[:emit_value]
|
|
when NilClass then 1
|
|
else
|
|
raise "The emit value specified is not recognized"
|
|
end
|
|
end
|
|
|
|
def conditions
|
|
" && (#{@options[:conditions]})" if @options[:conditions]
|
|
end
|
|
end
|
|
|
|
delegate :map_function, :map_body, :formatted_key, :to => :generator
|
|
|
|
def view_parameters
|
|
_super = super
|
|
if _super[:reduce]
|
|
_super
|
|
else
|
|
{:include_docs => true, :reduce => false}.merge(_super)
|
|
end
|
|
end
|
|
|
|
def reduce_function
|
|
"_sum"
|
|
end
|
|
|
|
def process_results(results)
|
|
processed = if count?
|
|
results['rows'].first.try(:[], 'value') || 0
|
|
else
|
|
results['rows'].map {|row|
|
|
row['doc'] || (row['id'] unless view_parameters[:include_docs])
|
|
}.compact
|
|
end
|
|
super processed
|
|
end
|
|
|
|
private
|
|
|
|
def generator
|
|
@generator ||= JavascriptGenerator.new(@options, @klass)
|
|
end
|
|
|
|
def count?
|
|
view_parameters[:reduce]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|