openlogic-couchrest_model 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +11 -0
- data/.rspec +4 -0
- data/Gemfile +4 -0
- data/LICENSE +176 -0
- data/README.md +137 -0
- data/Rakefile +38 -0
- data/THANKS.md +21 -0
- data/VERSION +1 -0
- data/benchmarks/dirty.rb +118 -0
- data/couchrest_model.gemspec +36 -0
- data/history.md +309 -0
- data/init.rb +1 -0
- data/lib/couchrest/model.rb +10 -0
- data/lib/couchrest/model/associations.rb +231 -0
- data/lib/couchrest/model/base.rb +129 -0
- data/lib/couchrest/model/callbacks.rb +28 -0
- data/lib/couchrest/model/casted_array.rb +83 -0
- data/lib/couchrest/model/casted_by.rb +33 -0
- data/lib/couchrest/model/casted_hash.rb +84 -0
- data/lib/couchrest/model/class_proxy.rb +135 -0
- data/lib/couchrest/model/collection.rb +273 -0
- data/lib/couchrest/model/configuration.rb +67 -0
- data/lib/couchrest/model/connection.rb +70 -0
- data/lib/couchrest/model/core_extensions/hash.rb +9 -0
- data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
- data/lib/couchrest/model/design_doc.rb +128 -0
- data/lib/couchrest/model/designs.rb +91 -0
- data/lib/couchrest/model/designs/view.rb +513 -0
- data/lib/couchrest/model/dirty.rb +39 -0
- data/lib/couchrest/model/document_queries.rb +99 -0
- data/lib/couchrest/model/embeddable.rb +78 -0
- data/lib/couchrest/model/errors.rb +25 -0
- data/lib/couchrest/model/extended_attachments.rb +83 -0
- data/lib/couchrest/model/persistence.rb +178 -0
- data/lib/couchrest/model/properties.rb +228 -0
- data/lib/couchrest/model/property.rb +114 -0
- data/lib/couchrest/model/property_protection.rb +71 -0
- data/lib/couchrest/model/proxyable.rb +183 -0
- data/lib/couchrest/model/support/couchrest_database.rb +13 -0
- data/lib/couchrest/model/support/couchrest_design.rb +33 -0
- data/lib/couchrest/model/typecast.rb +154 -0
- data/lib/couchrest/model/validations.rb +80 -0
- data/lib/couchrest/model/validations/casted_model.rb +16 -0
- data/lib/couchrest/model/validations/locale/en.yml +5 -0
- data/lib/couchrest/model/validations/uniqueness.rb +69 -0
- data/lib/couchrest/model/views.rb +151 -0
- data/lib/couchrest/railtie.rb +24 -0
- data/lib/couchrest_model.rb +66 -0
- data/lib/rails/generators/couchrest_model.rb +16 -0
- data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
- data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
- data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
- data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
- data/spec/.gitignore +1 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/config/couchdb.yml +10 -0
- data/spec/fixtures/models/article.rb +36 -0
- data/spec/fixtures/models/base.rb +164 -0
- data/spec/fixtures/models/card.rb +19 -0
- data/spec/fixtures/models/cat.rb +23 -0
- data/spec/fixtures/models/client.rb +6 -0
- data/spec/fixtures/models/course.rb +27 -0
- data/spec/fixtures/models/event.rb +8 -0
- data/spec/fixtures/models/invoice.rb +14 -0
- data/spec/fixtures/models/key_chain.rb +5 -0
- data/spec/fixtures/models/membership.rb +4 -0
- data/spec/fixtures/models/person.rb +11 -0
- data/spec/fixtures/models/project.rb +6 -0
- data/spec/fixtures/models/question.rb +7 -0
- data/spec/fixtures/models/sale_entry.rb +9 -0
- data/spec/fixtures/models/sale_invoice.rb +14 -0
- data/spec/fixtures/models/service.rb +10 -0
- data/spec/fixtures/models/user.rb +22 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/functional/validations_spec.rb +8 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/unit/active_model_lint_spec.rb +30 -0
- data/spec/unit/assocations_spec.rb +242 -0
- data/spec/unit/attachment_spec.rb +176 -0
- data/spec/unit/base_spec.rb +537 -0
- data/spec/unit/casted_spec.rb +72 -0
- data/spec/unit/class_proxy_spec.rb +167 -0
- data/spec/unit/collection_spec.rb +86 -0
- data/spec/unit/configuration_spec.rb +77 -0
- data/spec/unit/connection_spec.rb +148 -0
- data/spec/unit/core_extensions/time_parsing.rb +77 -0
- data/spec/unit/design_doc_spec.rb +241 -0
- data/spec/unit/designs/view_spec.rb +831 -0
- data/spec/unit/designs_spec.rb +134 -0
- data/spec/unit/dirty_spec.rb +436 -0
- data/spec/unit/embeddable_spec.rb +498 -0
- data/spec/unit/inherited_spec.rb +33 -0
- data/spec/unit/persistence_spec.rb +481 -0
- data/spec/unit/property_protection_spec.rb +192 -0
- data/spec/unit/property_spec.rb +481 -0
- data/spec/unit/proxyable_spec.rb +376 -0
- data/spec/unit/subclass_spec.rb +85 -0
- data/spec/unit/typecast_spec.rb +521 -0
- data/spec/unit/validations_spec.rb +140 -0
- data/spec/unit/view_spec.rb +367 -0
- metadata +301 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
#
|
2
|
+
# Extend CouchRest's normal database delete! method to ensure any caches are
|
3
|
+
# also emptied. Given that this is a rare event, and the consequences are not
|
4
|
+
# very severe, we just completely empty the cache.
|
5
|
+
#
|
6
|
+
CouchRest::Database.class_eval do
|
7
|
+
|
8
|
+
def delete!
|
9
|
+
Thread.current[:couchrest_design_cache] = { }
|
10
|
+
CouchRest.delete @root
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
CouchRest::Design.class_eval do
|
3
|
+
|
4
|
+
# Calculate and update the checksum of the Design document.
|
5
|
+
# Used for ensuring the latest version has been sent to the database.
|
6
|
+
#
|
7
|
+
# This will generate an flatterned, ordered array of all the elements of the
|
8
|
+
# design document, convert to string then generate an MD5 Hash. This should
|
9
|
+
# result in a consisitent Hash accross all platforms.
|
10
|
+
#
|
11
|
+
def checksum!
|
12
|
+
# create a copy of basic elements
|
13
|
+
base = self.dup
|
14
|
+
base.delete('_id')
|
15
|
+
base.delete('_rev')
|
16
|
+
base.delete('couchrest-hash')
|
17
|
+
result = nil
|
18
|
+
flatten =
|
19
|
+
lambda {|r|
|
20
|
+
(recurse = lambda {|v|
|
21
|
+
if v.is_a?(Hash) || v.is_a?(CouchRest::Document)
|
22
|
+
v.to_a.map{|v| recurse.call(v)}.flatten
|
23
|
+
elsif v.is_a?(Array)
|
24
|
+
v.flatten.map{|v| recurse.call(v)}
|
25
|
+
else
|
26
|
+
v.to_s
|
27
|
+
end
|
28
|
+
}).call(r)
|
29
|
+
}
|
30
|
+
self['couchrest-hash'] = Digest::MD5.hexdigest(flatten.call(base).sort.join(''))
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
module Typecast
|
4
|
+
|
5
|
+
def typecast_value(value, property) # klass, init_method)
|
6
|
+
return nil if value.nil?
|
7
|
+
klass = property.type_class
|
8
|
+
if value.instance_of?(klass) || klass == Object
|
9
|
+
if klass == Time && !value.utc?
|
10
|
+
value.utc # Ensure Time is always in UTC
|
11
|
+
else
|
12
|
+
value
|
13
|
+
end
|
14
|
+
elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
|
15
|
+
send('typecast_to_'+klass.to_s.downcase, value)
|
16
|
+
else
|
17
|
+
property.build(value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
# Typecast a value to an Integer
|
24
|
+
def typecast_to_integer(value)
|
25
|
+
typecast_to_numeric(value, :to_i)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Typecast a value to a String
|
29
|
+
def typecast_to_string(value)
|
30
|
+
value.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
# Typecast a value to a true or false
|
34
|
+
def typecast_to_trueclass(value)
|
35
|
+
if value.kind_of?(Integer)
|
36
|
+
return true if value == 1
|
37
|
+
return false if value == 0
|
38
|
+
elsif value.respond_to?(:to_s)
|
39
|
+
return true if %w[ true 1 t ].include?(value.to_s.downcase)
|
40
|
+
return false if %w[ false 0 f ].include?(value.to_s.downcase)
|
41
|
+
end
|
42
|
+
value
|
43
|
+
end
|
44
|
+
|
45
|
+
# Typecast a value to a BigDecimal
|
46
|
+
def typecast_to_bigdecimal(value)
|
47
|
+
if value.kind_of?(Integer)
|
48
|
+
value.to_s.to_d
|
49
|
+
else
|
50
|
+
typecast_to_numeric(value, :to_d)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Typecast a value to a Float
|
55
|
+
def typecast_to_float(value)
|
56
|
+
typecast_to_numeric(value, :to_f)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Match numeric string
|
60
|
+
def typecast_to_numeric(value, method)
|
61
|
+
if value.respond_to?(:to_str)
|
62
|
+
if value.strip.gsub(/,/, '.').gsub(/\.(?!\d*\Z)/, '').to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
|
63
|
+
$1.send(method)
|
64
|
+
else
|
65
|
+
value
|
66
|
+
end
|
67
|
+
elsif value.respond_to?(method)
|
68
|
+
value.send(method)
|
69
|
+
else
|
70
|
+
value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Typecasts an arbitrary value to a DateTime.
|
75
|
+
# Handles both Hashes and DateTime instances.
|
76
|
+
# This is slow!! Use Time instead.
|
77
|
+
def typecast_to_datetime(value)
|
78
|
+
if value.is_a?(Hash)
|
79
|
+
typecast_hash_to_datetime(value)
|
80
|
+
else
|
81
|
+
DateTime.parse(value.to_s)
|
82
|
+
end
|
83
|
+
rescue ArgumentError
|
84
|
+
value
|
85
|
+
end
|
86
|
+
|
87
|
+
# Typecasts an arbitrary value to a Date
|
88
|
+
# Handles both Hashes and Date instances.
|
89
|
+
def typecast_to_date(value)
|
90
|
+
if value.is_a?(Hash)
|
91
|
+
typecast_hash_to_date(value)
|
92
|
+
elsif value.is_a?(Time) # sometimes people think date is time!
|
93
|
+
value.to_date
|
94
|
+
elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
|
95
|
+
# Faster than parsing the date
|
96
|
+
Date.new($1.to_i, $2.to_i, $3.to_i)
|
97
|
+
else
|
98
|
+
Date.parse(value)
|
99
|
+
end
|
100
|
+
rescue ArgumentError
|
101
|
+
value
|
102
|
+
end
|
103
|
+
|
104
|
+
# Typecasts an arbitrary value to a Time
|
105
|
+
# Handles both Hashes and Time instances.
|
106
|
+
def typecast_to_time(value)
|
107
|
+
if value.is_a?(Hash)
|
108
|
+
typecast_hash_to_time(value)
|
109
|
+
else
|
110
|
+
Time.parse_iso8601(value.to_s)
|
111
|
+
end
|
112
|
+
rescue ArgumentError
|
113
|
+
value
|
114
|
+
rescue TypeError
|
115
|
+
value
|
116
|
+
end
|
117
|
+
|
118
|
+
# Creates a DateTime instance from a Hash with keys :year, :month, :day,
|
119
|
+
# :hour, :min, :sec
|
120
|
+
def typecast_hash_to_datetime(value)
|
121
|
+
DateTime.new(*extract_time(value))
|
122
|
+
end
|
123
|
+
|
124
|
+
# Creates a Date instance from a Hash with keys :year, :month, :day
|
125
|
+
def typecast_hash_to_date(value)
|
126
|
+
Date.new(*extract_time(value)[0, 3])
|
127
|
+
end
|
128
|
+
|
129
|
+
# Creates a Time instance from a Hash with keys :year, :month, :day,
|
130
|
+
# :hour, :min, :sec
|
131
|
+
def typecast_hash_to_time(value)
|
132
|
+
Time.utc(*extract_time(value))
|
133
|
+
end
|
134
|
+
|
135
|
+
# Extracts the given args from the hash. If a value does not exist, it
|
136
|
+
# uses the value of Time.now.
|
137
|
+
def extract_time(value)
|
138
|
+
now = Time.now
|
139
|
+
[:year, :month, :day, :hour, :min, :sec].map do |segment|
|
140
|
+
typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Typecast a value to a Class
|
145
|
+
def typecast_to_class(value)
|
146
|
+
value.to_s.constantize
|
147
|
+
rescue NameError
|
148
|
+
value
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "couchrest/model/validations/casted_model"
|
4
|
+
require "couchrest/model/validations/uniqueness"
|
5
|
+
|
6
|
+
I18n.load_path << File.join(
|
7
|
+
File.dirname(__FILE__), "validations", "locale", "en.yml"
|
8
|
+
)
|
9
|
+
|
10
|
+
module CouchRest
|
11
|
+
module Model
|
12
|
+
|
13
|
+
# Validations may be applied to both Model::Base and Model::CastedModel
|
14
|
+
module Validations
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
include ActiveModel::Validations
|
17
|
+
|
18
|
+
# Determine if the document is valid.
|
19
|
+
#
|
20
|
+
# @example Is the document valid?
|
21
|
+
# person.valid?
|
22
|
+
#
|
23
|
+
# @example Is the document valid in a context?
|
24
|
+
# person.valid?(:create)
|
25
|
+
#
|
26
|
+
# @param [ Symbol ] context The optional validation context.
|
27
|
+
#
|
28
|
+
# @return [ true, false ] True if valid, false if not.
|
29
|
+
#
|
30
|
+
def valid?(context = nil)
|
31
|
+
super context ? context : (new? ? :create : :update)
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
|
36
|
+
# Validates the associated casted model. This method should not be
|
37
|
+
# used within your code as it is automatically included when a CastedModel
|
38
|
+
# is used inside the model.
|
39
|
+
def validates_casted_model(*args)
|
40
|
+
validates_with(CastedModelValidator, _merge_attributes(args))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Validates if the field is unique for this type of document. Automatically creates
|
44
|
+
# a view if one does not already exist and performs a search for all matching
|
45
|
+
# documents.
|
46
|
+
#
|
47
|
+
# Example:
|
48
|
+
#
|
49
|
+
# class Person < CouchRest::Model::Base
|
50
|
+
# property :title, String
|
51
|
+
#
|
52
|
+
# validates_uniqueness_of :title
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# Asside from the standard options, you can specify the name of the view you'd like
|
56
|
+
# to use for the search inside the +:view+ option. The following example would search
|
57
|
+
# for the code in side the +all+ view, useful for when +unique_id+ is used and you'd
|
58
|
+
# like to check before receiving a RestClient Conflict error:
|
59
|
+
#
|
60
|
+
# validates_uniqueness_of :code, :view => 'all'
|
61
|
+
#
|
62
|
+
# A +:proxy+ parameter is also accepted if you would
|
63
|
+
# like to call a method on the document on which the view should be performed.
|
64
|
+
#
|
65
|
+
# For Example:
|
66
|
+
#
|
67
|
+
# # Same as not including proxy:
|
68
|
+
# validates_uniqueness_of :title, :proxy => 'class'
|
69
|
+
#
|
70
|
+
# # Person#company.people provides a proxy object for people
|
71
|
+
# validates_uniqueness_of :title, :proxy => 'company.people'
|
72
|
+
#
|
73
|
+
def validates_uniqueness_of(*args)
|
74
|
+
validates_with(UniquenessValidator, _merge_attributes(args))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
module Validations
|
4
|
+
class CastedModelValidator < ActiveModel::EachValidator
|
5
|
+
|
6
|
+
def validate_each(document, attribute, value)
|
7
|
+
values = value.is_a?(Array) ? value : [value]
|
8
|
+
return if values.collect {|doc| doc.nil? || doc.valid? }.all?
|
9
|
+
error_options = { :value => value }
|
10
|
+
error_options[:message] = options[:message] if options[:message]
|
11
|
+
document.errors.add(attribute, :invalid, error_options)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module CouchRest
|
4
|
+
module Model
|
5
|
+
module Validations
|
6
|
+
|
7
|
+
# Validates if a field is unique
|
8
|
+
class UniquenessValidator < ActiveModel::EachValidator
|
9
|
+
|
10
|
+
# Ensure we have a class available so we can check for a usable view
|
11
|
+
# or add one if necessary.
|
12
|
+
def setup(model)
|
13
|
+
@model = model
|
14
|
+
if options[:view].blank?
|
15
|
+
attributes.each do |attribute|
|
16
|
+
opts = merge_view_options(attribute)
|
17
|
+
|
18
|
+
if model.respond_to?(:has_view?) && !model.has_view?(opts[:view_name])
|
19
|
+
opts[:keys] << {:allow_nil => true}
|
20
|
+
model.view_by(*opts[:keys])
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate_each(document, attribute, value)
|
27
|
+
opts = merge_view_options(attribute)
|
28
|
+
|
29
|
+
values = opts[:keys].map{|k| document.send(k)}
|
30
|
+
values = values.first if values.length == 1
|
31
|
+
|
32
|
+
model = (document.respond_to?(:model_proxy) && document.model_proxy ? document.model_proxy : @model)
|
33
|
+
# Determine the base of the search
|
34
|
+
base = opts[:proxy].nil? ? model : document.instance_eval(opts[:proxy])
|
35
|
+
|
36
|
+
if base.respond_to?(:has_view?) && !base.has_view?(opts[:view_name])
|
37
|
+
raise "View #{document.class.name}.#{opts[:view_name]} does not exist for validation!"
|
38
|
+
end
|
39
|
+
|
40
|
+
rows = base.view(opts[:view_name], :key => values, :limit => 2, :include_docs => false)['rows']
|
41
|
+
return if rows.empty?
|
42
|
+
|
43
|
+
unless document.new?
|
44
|
+
return if rows.find{|row| row['id'] == document.id}
|
45
|
+
end
|
46
|
+
|
47
|
+
if rows.length > 0
|
48
|
+
opts = options.merge(:value => value)
|
49
|
+
opts.delete(:scope) # Has meaning with I18n!
|
50
|
+
document.errors.add(attribute, :taken, opts)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def merge_view_options(attr)
|
57
|
+
keys = [attr]
|
58
|
+
keys.unshift(*options[:scope]) unless options[:scope].nil?
|
59
|
+
|
60
|
+
view_name = options[:view].nil? ? "by_#{keys.join('_and_')}" : options[:view]
|
61
|
+
|
62
|
+
options.merge({:keys => keys, :view_name => view_name})
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
module Views
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
# Define a CouchDB view. The name of the view will be the concatenation
|
8
|
+
# of <tt>by</tt> and the keys joined by <tt>_and_</tt>
|
9
|
+
#
|
10
|
+
# ==== Example views:
|
11
|
+
#
|
12
|
+
# class Post
|
13
|
+
# # view with default options
|
14
|
+
# # query with Post.by_date
|
15
|
+
# view_by :date, :descending => true
|
16
|
+
#
|
17
|
+
# # view with compound sort-keys
|
18
|
+
# # query with Post.by_user_id_and_date
|
19
|
+
# view_by :user_id, :date
|
20
|
+
#
|
21
|
+
# # view with custom map/reduce functions
|
22
|
+
# # query with Post.by_tags :reduce => true
|
23
|
+
# view_by :tags,
|
24
|
+
# :map =>
|
25
|
+
# "function(doc) {
|
26
|
+
# if (doc['model'] == 'Post' && doc.tags) {
|
27
|
+
# doc.tags.forEach(function(tag){
|
28
|
+
# emit(doc.tag, 1);
|
29
|
+
# });
|
30
|
+
# }
|
31
|
+
# }",
|
32
|
+
# :reduce =>
|
33
|
+
# "function(keys, values, rereduce) {
|
34
|
+
# return sum(values);
|
35
|
+
# }"
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# <tt>view_by :date</tt> will create a view defined by this Javascript
|
39
|
+
# function:
|
40
|
+
#
|
41
|
+
# function(doc) {
|
42
|
+
# if (doc['model'] == 'Post' && doc.date) {
|
43
|
+
# emit(doc.date, null);
|
44
|
+
# }
|
45
|
+
# }
|
46
|
+
#
|
47
|
+
# It can be queried by calling <tt>Post.by_date</tt> which accepts all
|
48
|
+
# valid options for CouchRest::Database#view. In addition, calling with
|
49
|
+
# the <tt>:raw => true</tt> option will return the view rows
|
50
|
+
# themselves. By default <tt>Post.by_date</tt> will return the
|
51
|
+
# documents included in the generated view.
|
52
|
+
#
|
53
|
+
# Calling with :database => [instance of CouchRest::Database] will
|
54
|
+
# send the query to a specific database, otherwise it will go to
|
55
|
+
# the model's default database (use_database)
|
56
|
+
#
|
57
|
+
# CouchRest::Database#view options can be applied at view definition
|
58
|
+
# time as defaults, and they will be curried and used at view query
|
59
|
+
# time. Or they can be overridden at query time.
|
60
|
+
#
|
61
|
+
# Custom views can be queried with <tt>:reduce => true</tt> to return
|
62
|
+
# reduce results. The default for custom views is to query with
|
63
|
+
# <tt>:reduce => false</tt>.
|
64
|
+
#
|
65
|
+
# Views are generated (on a per-model basis) lazily on first-access.
|
66
|
+
# This means that if you are deploying changes to a view, the views for
|
67
|
+
# that model won't be available until generation is complete. This can
|
68
|
+
# take some time with large databases. Strategies are in the works.
|
69
|
+
#
|
70
|
+
# To understand the capabilities of this view system more completely,
|
71
|
+
# it is recommended that you read the RSpec file at
|
72
|
+
# <tt>spec/couchrest/more/extended_doc_spec.rb</tt>.
|
73
|
+
|
74
|
+
def view_by(*keys)
|
75
|
+
return unless auto_update_design_doc
|
76
|
+
|
77
|
+
opts = keys.pop if keys.last.is_a?(Hash)
|
78
|
+
opts ||= {}
|
79
|
+
ducktype = opts.delete(:ducktype)
|
80
|
+
|
81
|
+
unless ducktype || opts[:map]
|
82
|
+
opts[:guards] ||= []
|
83
|
+
opts[:guards].push "(doc['#{model_type_key}'] == '#{self.to_s}')"
|
84
|
+
end
|
85
|
+
keys.push opts
|
86
|
+
design_doc.view_by(*keys)
|
87
|
+
end
|
88
|
+
|
89
|
+
# returns stored defaults if there is a view named this in the design doc
|
90
|
+
def has_view?(name)
|
91
|
+
design_doc && design_doc.has_view?(name)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Check if the view can be reduced by checking to see if it has a
|
95
|
+
# reduce function.
|
96
|
+
def can_reduce_view?(name)
|
97
|
+
design_doc && design_doc.can_reduce_view?(name)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Dispatches to any named view.
|
101
|
+
def view(name, query={}, &block)
|
102
|
+
query = query.dup # Modifications made on copy!
|
103
|
+
db = query.delete(:database) || database
|
104
|
+
query[:raw] = true if query[:reduce]
|
105
|
+
raw = query.delete(:raw)
|
106
|
+
save_design_doc(db)
|
107
|
+
fetch_view_with_docs(db, name, query, raw, &block)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Find the first entry in the view. If the second parameter is a string
|
111
|
+
# it will be used as the key for the request, for example:
|
112
|
+
#
|
113
|
+
# Course.first_from_view('by_teacher', 'Fred')
|
114
|
+
#
|
115
|
+
# More advanced requests can be performed by providing a hash:
|
116
|
+
#
|
117
|
+
# Course.first_from_view('by_teacher', :startkey => 'bbb', :endkey => 'eee')
|
118
|
+
#
|
119
|
+
def first_from_view(name, *args)
|
120
|
+
query = {:limit => 1}
|
121
|
+
case args.first
|
122
|
+
when String, Array
|
123
|
+
query.update(args[1]) unless args[1].nil?
|
124
|
+
query[:key] = args.first
|
125
|
+
when Hash
|
126
|
+
query.update(args.first)
|
127
|
+
end
|
128
|
+
view(name, query).first
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def fetch_view_with_docs(db, name, opts, raw=false, &block)
|
134
|
+
if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
|
135
|
+
fetch_view(db, name, opts, &block)
|
136
|
+
else
|
137
|
+
opts = opts.merge(:include_docs => true)
|
138
|
+
view = fetch_view db, name, opts, &block
|
139
|
+
view['rows'].collect{|r| build_from_database(r['doc'])} if view['rows']
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def fetch_view(db, view_name, opts, &block)
|
144
|
+
raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
|
145
|
+
design_doc.view_on(db, view_name, opts, &block)
|
146
|
+
end
|
147
|
+
end # module ClassMethods
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|