openlogic-couchrest_model 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|