couchrest_model 1.0.0 → 1.1.0.beta
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 +1 -1
- data/Gemfile.lock +19 -20
- data/README.md +145 -20
- data/VERSION +1 -1
- data/couchrest_model.gemspec +2 -3
- data/history.txt +14 -0
- data/lib/couchrest/model/associations.rb +4 -4
- data/lib/couchrest/model/base.rb +5 -0
- data/lib/couchrest/model/callbacks.rb +1 -2
- data/lib/couchrest/model/collection.rb +1 -1
- data/lib/couchrest/model/designs/view.rb +486 -0
- data/lib/couchrest/model/designs.rb +81 -0
- data/lib/couchrest/model/document_queries.rb +1 -1
- data/lib/couchrest/model/persistence.rb +25 -16
- data/lib/couchrest/model/properties.rb +5 -1
- data/lib/couchrest/model/property.rb +2 -2
- data/lib/couchrest/model/proxyable.rb +152 -0
- data/lib/couchrest/model/typecast.rb +1 -1
- data/lib/couchrest/model/validations/casted_model.rb +3 -1
- data/lib/couchrest/model/validations/locale/en.yml +1 -1
- data/lib/couchrest/model/validations/uniqueness.rb +6 -7
- data/lib/couchrest/model/validations.rb +1 -0
- data/lib/couchrest/model/views.rb +11 -9
- data/lib/couchrest_model.rb +3 -0
- data/spec/couchrest/assocations_spec.rb +2 -2
- data/spec/couchrest/base_spec.rb +15 -1
- data/spec/couchrest/casted_model_spec.rb +30 -12
- data/spec/couchrest/class_proxy_spec.rb +2 -2
- data/spec/couchrest/collection_spec.rb +89 -0
- data/spec/couchrest/designs/view_spec.rb +766 -0
- data/spec/couchrest/designs_spec.rb +110 -0
- data/spec/couchrest/persistence_spec.rb +36 -7
- data/spec/couchrest/property_spec.rb +15 -0
- data/spec/couchrest/proxyable_spec.rb +329 -0
- data/spec/couchrest/{validations.rb → validations_spec.rb} +1 -3
- data/spec/couchrest/view_spec.rb +19 -91
- data/spec/fixtures/base.rb +8 -6
- data/spec/fixtures/more/article.rb +1 -1
- data/spec/fixtures/more/course.rb +4 -2
- metadata +21 -76
- data/lib/couchrest/model/view.rb +0 -190
@@ -0,0 +1,486 @@
|
|
1
|
+
module CouchRest
|
2
|
+
module Model
|
3
|
+
module Designs
|
4
|
+
|
5
|
+
#
|
6
|
+
# A proxy class that allows view queries to be created using
|
7
|
+
# chained method calls. After each call a new instance of the method
|
8
|
+
# is created based on the original in a similar fashion to ruby's Sequel
|
9
|
+
# library, or Rails 3's Arel.
|
10
|
+
#
|
11
|
+
# CouchDB views have inherent limitations, so joins and filters as used in
|
12
|
+
# a normal relational database are not possible.
|
13
|
+
#
|
14
|
+
class View
|
15
|
+
include Enumerable
|
16
|
+
|
17
|
+
attr_accessor :owner, :model, :name, :query, :result
|
18
|
+
|
19
|
+
# Initialize a new View object. This method should not be called from
|
20
|
+
# outside CouchRest Model.
|
21
|
+
def initialize(parent, new_query = {}, name = nil)
|
22
|
+
if parent.is_a?(Class) && parent < CouchRest::Model::Base
|
23
|
+
raise "Name must be provided for view to be initialized" if name.nil?
|
24
|
+
self.model = parent
|
25
|
+
self.owner = parent
|
26
|
+
self.name = name.to_s
|
27
|
+
# Default options:
|
28
|
+
self.query = { :reduce => false }
|
29
|
+
elsif parent.is_a?(self.class)
|
30
|
+
self.model = (new_query.delete(:proxy) || parent.model)
|
31
|
+
self.owner = parent.owner
|
32
|
+
self.name = parent.name
|
33
|
+
self.query = parent.query.dup
|
34
|
+
else
|
35
|
+
raise "View cannot be initialized without a parent Model or View"
|
36
|
+
end
|
37
|
+
query.update(new_query)
|
38
|
+
super()
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# == View Execution Methods
|
43
|
+
#
|
44
|
+
# Request to the CouchDB database using the current query values.
|
45
|
+
|
46
|
+
# Return each row wrapped in a ViewRow object. Unlike the raw
|
47
|
+
# CouchDB request, this will provide an empty array if there
|
48
|
+
# are no results.
|
49
|
+
def rows
|
50
|
+
return @rows if @rows
|
51
|
+
if execute && result['rows']
|
52
|
+
@rows ||= result['rows'].map{|v| ViewRow.new(v, model)}
|
53
|
+
else
|
54
|
+
[ ]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Fetch all the documents the view can access. If the view has
|
59
|
+
# not already been prepared for including documents in the query,
|
60
|
+
# it will be added automatically and reset any previously cached
|
61
|
+
# results.
|
62
|
+
def all
|
63
|
+
include_docs!
|
64
|
+
docs
|
65
|
+
end
|
66
|
+
|
67
|
+
# Provide all the documents from the view. If the view has not been
|
68
|
+
# prepared with the +include_docs+ option, each document will be
|
69
|
+
# loaded individually.
|
70
|
+
def docs
|
71
|
+
@docs ||= rows.map{|r| r.doc}
|
72
|
+
end
|
73
|
+
|
74
|
+
# If another request has been made on the view, this will return
|
75
|
+
# the first document in the set. If not, a new query object will be
|
76
|
+
# generated with a limit of 1 so that only the first document is
|
77
|
+
# loaded.
|
78
|
+
def first
|
79
|
+
result ? all.first : limit(1).all.first
|
80
|
+
end
|
81
|
+
|
82
|
+
# Same as first but will order the view in descending order. This
|
83
|
+
# does not however reverse the search keys or the offset, so if you
|
84
|
+
# are using a +startkey+ and +endkey+ you might end up with
|
85
|
+
# unexpected results.
|
86
|
+
#
|
87
|
+
# If in doubt, don't use this method!
|
88
|
+
#
|
89
|
+
def last
|
90
|
+
result ? all.last : limit(1).descending.all.last
|
91
|
+
end
|
92
|
+
|
93
|
+
# Perform a count operation based on the current view. If the view
|
94
|
+
# can be reduced, the reduce will be performed and return the first
|
95
|
+
# value. This is okay for most simple queries, but may provide
|
96
|
+
# unexpected results if your reduce method does not calculate
|
97
|
+
# the total number of documents in a result set.
|
98
|
+
#
|
99
|
+
# Trying to use this method with the group option will raise an error.
|
100
|
+
#
|
101
|
+
# If no reduce function is defined, a query will be performed
|
102
|
+
# to return the total number of rows, this is the equivalant of:
|
103
|
+
#
|
104
|
+
# view.limit(0).total_rows
|
105
|
+
#
|
106
|
+
def count
|
107
|
+
raise "View#count cannot be used with group options" if query[:group]
|
108
|
+
if can_reduce?
|
109
|
+
row = reduce.rows.first
|
110
|
+
row.nil? ? 0 : row.value
|
111
|
+
else
|
112
|
+
limit(0).total_rows
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Check to see if the array of documents is empty. This *will*
|
117
|
+
# perform the query and return all documents ready to use, if you don't
|
118
|
+
# want to load anything, use +#total_rows+ or +#count+ instead.
|
119
|
+
def empty?
|
120
|
+
all.empty?
|
121
|
+
end
|
122
|
+
|
123
|
+
# Run through each document provided by the +#all+ method.
|
124
|
+
# This is also used by the Enumerator mixin to provide all the standard
|
125
|
+
# ruby collection directly on the view.
|
126
|
+
def each(&block)
|
127
|
+
all.each(&block)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Wrapper for the results offset. As per the CouchDB API,
|
131
|
+
# this may be nil if groups are used.
|
132
|
+
def offset
|
133
|
+
execute['offset']
|
134
|
+
end
|
135
|
+
|
136
|
+
# Wrapper for the total_rows value provided by the query. As per the
|
137
|
+
# CouchDB API, this may be nil if groups are used.
|
138
|
+
def total_rows
|
139
|
+
execute['total_rows']
|
140
|
+
end
|
141
|
+
|
142
|
+
# Convenience wrapper around the rows result set. This will provide
|
143
|
+
# and array of keys.
|
144
|
+
def keys
|
145
|
+
rows.map{|r| r.key}
|
146
|
+
end
|
147
|
+
|
148
|
+
# Convenience wrapper to provide all the values from the route
|
149
|
+
# set without having to go through +rows+.
|
150
|
+
def values
|
151
|
+
rows.map{|r| r.value}
|
152
|
+
end
|
153
|
+
|
154
|
+
# Accept requests as if the view was an array. Used for backwards compatibity
|
155
|
+
# with older queries:
|
156
|
+
#
|
157
|
+
# Model.all(:raw => true, :limit => 0)['total_rows']
|
158
|
+
#
|
159
|
+
# In this example, the raw option will be ignored, and the total rows
|
160
|
+
# will still be accessible.
|
161
|
+
#
|
162
|
+
def [](value)
|
163
|
+
execute[value]
|
164
|
+
end
|
165
|
+
|
166
|
+
# No yet implemented. Eventually this will provide a raw hash
|
167
|
+
# of the information CouchDB holds about the view.
|
168
|
+
def info
|
169
|
+
raise "Not yet implemented"
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
# == View Filter Methods
|
174
|
+
#
|
175
|
+
# View filters return a copy of the view instance with the query
|
176
|
+
# modified appropriatly. Errors will be raised if the methods
|
177
|
+
# are combined in an incorrect fashion.
|
178
|
+
#
|
179
|
+
|
180
|
+
# Find all entries in the index whose key matches the value provided.
|
181
|
+
#
|
182
|
+
# Cannot be used when the +#startkey+ or +#endkey+ have been set.
|
183
|
+
def key(value)
|
184
|
+
raise "View#key cannot be used when startkey or endkey have been set" unless query[:startkey].nil? && query[:endkey].nil?
|
185
|
+
update_query(:key => value)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Find all index keys that start with the value provided. May or may
|
189
|
+
# not be used in conjunction with the +endkey+ option.
|
190
|
+
#
|
191
|
+
# When the +#descending+ option is used (not the default), the start
|
192
|
+
# and end keys should be reversed, as per the CouchDB API.
|
193
|
+
#
|
194
|
+
# Cannot be used if the key has been set.
|
195
|
+
def startkey(value)
|
196
|
+
raise "View#startkey cannot be used when key has been set" unless query[:key].nil?
|
197
|
+
update_query(:startkey => value)
|
198
|
+
end
|
199
|
+
|
200
|
+
# The result set should start from the position of the provided document.
|
201
|
+
# The value may be provided as an object that responds to the +#id+ call
|
202
|
+
# or a string.
|
203
|
+
def startkey_doc(value)
|
204
|
+
update_query(:startkey_docid => value.is_a?(String) ? value : value.id)
|
205
|
+
end
|
206
|
+
|
207
|
+
# The opposite of +#startkey+, finds all index entries whose key is before
|
208
|
+
# the value specified.
|
209
|
+
#
|
210
|
+
# See the +#startkey+ method for more details and the +#inclusive_end+
|
211
|
+
# option.
|
212
|
+
def endkey(value)
|
213
|
+
raise "View#endkey cannot be used when key has been set" unless query[:key].nil?
|
214
|
+
update_query(:endkey => value)
|
215
|
+
end
|
216
|
+
|
217
|
+
# The result set should end at the position of the provided document.
|
218
|
+
# The value may be provided as an object that responds to the +#id+
|
219
|
+
# call or a string.
|
220
|
+
def endkey_doc(value)
|
221
|
+
update_query(:endkey_docid => value.is_a?(String) ? value : value.id)
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
# The results should be provided in descending order.
|
226
|
+
#
|
227
|
+
# Descending is false by default, this method will enable it and cannot
|
228
|
+
# be undone.
|
229
|
+
def descending
|
230
|
+
update_query(:descending => true)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Limit the result set to the value supplied.
|
234
|
+
def limit(value)
|
235
|
+
update_query(:limit => value)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Skip the number of entries in the index specified by value. This would be
|
239
|
+
# the equivilent of an offset in SQL.
|
240
|
+
#
|
241
|
+
# The CouchDB documentation states that the skip option should not be used
|
242
|
+
# with large data sets as it is inefficient. Use the +startkey_doc+ method
|
243
|
+
# instead to skip ranges efficiently.
|
244
|
+
def skip(value = 0)
|
245
|
+
update_query(:skip => value)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Use the reduce function on the view. If none is available this method
|
249
|
+
# will fail.
|
250
|
+
def reduce
|
251
|
+
raise "Cannot reduce a view without a reduce method" unless can_reduce?
|
252
|
+
update_query(:reduce => true, :include_docs => nil)
|
253
|
+
end
|
254
|
+
|
255
|
+
# Control whether the reduce function reduces to a set of distinct keys
|
256
|
+
# or to a single result row.
|
257
|
+
#
|
258
|
+
# By default the value is false, and can only be set when the view's
|
259
|
+
# +#reduce+ option has been set.
|
260
|
+
def group
|
261
|
+
raise "View#reduce must have been set before grouping is permitted" unless query[:reduce]
|
262
|
+
update_query(:group => true)
|
263
|
+
end
|
264
|
+
|
265
|
+
# Will set the level the grouping should be performed to. As per the
|
266
|
+
# CouchDB API, it only makes sense when the index key is an array.
|
267
|
+
#
|
268
|
+
# This will automatically set the group option.
|
269
|
+
def group_level(value)
|
270
|
+
group.update_query(:group_level => value.to_i)
|
271
|
+
end
|
272
|
+
|
273
|
+
def include_docs
|
274
|
+
update_query.include_docs!
|
275
|
+
end
|
276
|
+
|
277
|
+
### Special View Filter Methods
|
278
|
+
|
279
|
+
# Specify the database the view should use. If not defined,
|
280
|
+
# an attempt will be made to load its value from the model.
|
281
|
+
def database(value)
|
282
|
+
update_query(:database => value)
|
283
|
+
end
|
284
|
+
|
285
|
+
# Set the view's proxy that will be used instead of the model
|
286
|
+
# for any future searches. As soon as this enters the
|
287
|
+
# new object's initializer it will be removed and replace
|
288
|
+
# the model object.
|
289
|
+
#
|
290
|
+
# See the Proxyable mixin for more details.
|
291
|
+
#
|
292
|
+
def proxy(value)
|
293
|
+
update_query(:proxy => value)
|
294
|
+
end
|
295
|
+
|
296
|
+
# Return any cached values to their nil state so that any queries
|
297
|
+
# requested later will have a fresh set of data.
|
298
|
+
def reset!
|
299
|
+
self.result = nil
|
300
|
+
@rows = nil
|
301
|
+
@docs = nil
|
302
|
+
end
|
303
|
+
|
304
|
+
# == Kaminari compatible pagination support
|
305
|
+
#
|
306
|
+
# Based on the really simple support for scoped pagination in the
|
307
|
+
# the Kaminari gem, we provide compatible methods here to perform
|
308
|
+
# the same actions you'd expect.
|
309
|
+
#
|
310
|
+
|
311
|
+
def page(page)
|
312
|
+
limit(owner.default_per_page).skip(owner.default_per_page * ([page.to_i, 1].max - 1))
|
313
|
+
end
|
314
|
+
|
315
|
+
def per(num)
|
316
|
+
raise "View#page must be called before #per!" if limit_value.nil? || offset_value.nil?
|
317
|
+
if (n = num.to_i) <= 0
|
318
|
+
self
|
319
|
+
else
|
320
|
+
limit(num).skip(offset_value / limit_value * n)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def total_count
|
325
|
+
@total_count ||= limit(nil).skip(nil).count
|
326
|
+
end
|
327
|
+
|
328
|
+
def offset_value
|
329
|
+
query[:skip]
|
330
|
+
end
|
331
|
+
|
332
|
+
def limit_value
|
333
|
+
query[:limit]
|
334
|
+
end
|
335
|
+
|
336
|
+
def num_pages
|
337
|
+
(total_count.to_f / limit_value).ceil
|
338
|
+
end
|
339
|
+
|
340
|
+
def current_page
|
341
|
+
(offset_value / limit_value) + 1
|
342
|
+
end
|
343
|
+
|
344
|
+
|
345
|
+
|
346
|
+
protected
|
347
|
+
|
348
|
+
def include_docs!
|
349
|
+
raise "Cannot include documents in view that has been reduced!" if query[:reduce]
|
350
|
+
reset! if result && !include_docs?
|
351
|
+
query[:include_docs] = true
|
352
|
+
self
|
353
|
+
end
|
354
|
+
|
355
|
+
def include_docs?
|
356
|
+
!!query[:include_docs]
|
357
|
+
end
|
358
|
+
|
359
|
+
def update_query(new_query = {})
|
360
|
+
self.class.new(self, new_query)
|
361
|
+
end
|
362
|
+
|
363
|
+
def design_doc
|
364
|
+
model.design_doc
|
365
|
+
end
|
366
|
+
|
367
|
+
def can_reduce?
|
368
|
+
!design_doc['views'][name]['reduce'].blank?
|
369
|
+
end
|
370
|
+
|
371
|
+
def use_database
|
372
|
+
query[:database] || model.database
|
373
|
+
end
|
374
|
+
|
375
|
+
def execute
|
376
|
+
return self.result if result
|
377
|
+
raise "Database must be defined in model or view!" if use_database.nil?
|
378
|
+
retryable = true
|
379
|
+
# Remove the reduce value if its not needed
|
380
|
+
query.delete(:reduce) unless can_reduce?
|
381
|
+
begin
|
382
|
+
self.result = model.design_doc.view_on(use_database, name, query.reject{|k,v| v.nil?})
|
383
|
+
rescue RestClient::ResourceNotFound => e
|
384
|
+
if retryable
|
385
|
+
model.save_design_doc(use_database)
|
386
|
+
retryable = false
|
387
|
+
retry
|
388
|
+
else
|
389
|
+
raise e
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
# Class Methods
|
395
|
+
class << self
|
396
|
+
|
397
|
+
# Simplified view creation. A new view will be added to the
|
398
|
+
# provided model's design document using the name and options.
|
399
|
+
#
|
400
|
+
# If the view name starts with "by_" and +:by+ is not provided in
|
401
|
+
# the options, the new view's map method will be interpretted and
|
402
|
+
# generated automatically. For example:
|
403
|
+
#
|
404
|
+
# View.create(Meeting, "by_date_and_name")
|
405
|
+
#
|
406
|
+
# Will create a view that searches by the date and name properties.
|
407
|
+
# Explicity setting the attributes to use is possible using the
|
408
|
+
# +:by+ option. For example:
|
409
|
+
#
|
410
|
+
# View.create(Meeting, "by_date_and_name", :by => [:date, :firstname, :lastname])
|
411
|
+
#
|
412
|
+
# The view name is the same, but three keys would be used in the
|
413
|
+
# subsecuent index.
|
414
|
+
#
|
415
|
+
def create(model, name, opts = {})
|
416
|
+
|
417
|
+
unless opts[:map]
|
418
|
+
if opts[:by].nil? && name.to_s =~ /^by_(.+)/
|
419
|
+
opts[:by] = $1.split(/_and_/)
|
420
|
+
end
|
421
|
+
|
422
|
+
raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
|
423
|
+
|
424
|
+
opts[:guards] ||= []
|
425
|
+
opts[:guards].push "(doc['#{model.model_type_key}'] == '#{model.to_s}')"
|
426
|
+
|
427
|
+
keys = opts[:by].map{|o| "doc['#{o}']"}
|
428
|
+
emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]"
|
429
|
+
opts[:guards] += keys.map{|k| "(#{k} != null)"}
|
430
|
+
opts[:map] = <<-EOF
|
431
|
+
function(doc) {
|
432
|
+
if (#{opts[:guards].join(' && ')}) {
|
433
|
+
emit(#{emit}, 1);
|
434
|
+
}
|
435
|
+
}
|
436
|
+
EOF
|
437
|
+
opts[:reduce] = <<-EOF
|
438
|
+
function(key, values, rereduce) {
|
439
|
+
return sum(values);
|
440
|
+
}
|
441
|
+
EOF
|
442
|
+
end
|
443
|
+
|
444
|
+
model.design_doc['views'] ||= {}
|
445
|
+
view = model.design_doc['views'][name.to_s] = { }
|
446
|
+
view['map'] = opts[:map]
|
447
|
+
view['reduce'] = opts[:reduce] if opts[:reduce]
|
448
|
+
view
|
449
|
+
end
|
450
|
+
|
451
|
+
end
|
452
|
+
|
453
|
+
end
|
454
|
+
|
455
|
+
# A special wrapper class that provides easy access to the key
|
456
|
+
# fields in a result row.
|
457
|
+
class ViewRow < Hash
|
458
|
+
attr_reader :model
|
459
|
+
def initialize(hash, model)
|
460
|
+
@model = model
|
461
|
+
replace(hash)
|
462
|
+
end
|
463
|
+
def id
|
464
|
+
self["id"]
|
465
|
+
end
|
466
|
+
def key
|
467
|
+
self["key"]
|
468
|
+
end
|
469
|
+
def value
|
470
|
+
self['value']
|
471
|
+
end
|
472
|
+
def raw_doc
|
473
|
+
self['doc']
|
474
|
+
end
|
475
|
+
# Send a request for the linked document either using the "id" field's
|
476
|
+
# value, or the ["value"]["_id"] used for linked documents.
|
477
|
+
def doc
|
478
|
+
return model.build_from_database(self['doc']) if self['doc']
|
479
|
+
doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id
|
480
|
+
doc_id ? model.get(doc_id) : nil
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
|
2
|
+
#### NOTE Work in progress! Not yet used!
|
3
|
+
|
4
|
+
module CouchRest
|
5
|
+
module Model
|
6
|
+
|
7
|
+
# A design block in CouchRest Model groups together the functionality of CouchDB's
|
8
|
+
# design documents in a simple block definition.
|
9
|
+
#
|
10
|
+
# class Person < CouchRest::Model::Base
|
11
|
+
# property :name
|
12
|
+
# timestamps!
|
13
|
+
#
|
14
|
+
# design do
|
15
|
+
# view :by_name
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
module Designs
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
|
24
|
+
# Add views and other design document features
|
25
|
+
# to the current model.
|
26
|
+
def design(*args, &block)
|
27
|
+
mapper = DesignMapper.new(self)
|
28
|
+
mapper.create_view_method(:all)
|
29
|
+
|
30
|
+
mapper.instance_eval(&block) if block_given?
|
31
|
+
|
32
|
+
req_design_doc_refresh
|
33
|
+
end
|
34
|
+
|
35
|
+
# Override the default page pagination value:
|
36
|
+
#
|
37
|
+
# class Person < CouchRest::Model::Base
|
38
|
+
# paginates_per 10
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
def paginates_per(val)
|
42
|
+
@_default_per_page = val
|
43
|
+
end
|
44
|
+
|
45
|
+
# The models number of documents to return
|
46
|
+
# by default when performing pagination.
|
47
|
+
# Returns 25 unless explicitly overridden via <tt>paginates_per</tt>
|
48
|
+
def default_per_page
|
49
|
+
@_default_per_page || 25
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
class DesignMapper
|
56
|
+
|
57
|
+
attr_accessor :model
|
58
|
+
|
59
|
+
def initialize(model)
|
60
|
+
self.model = model
|
61
|
+
end
|
62
|
+
|
63
|
+
# Define a view and generate a method that will provide a new
|
64
|
+
# View instance when requested.
|
65
|
+
def view(name, opts = {})
|
66
|
+
View.create(model, name, opts)
|
67
|
+
create_view_method(name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_view_method(name)
|
71
|
+
model.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
72
|
+
def self.#{name}(opts = {})
|
73
|
+
CouchRest::Model::Designs::View.new(self, opts, '#{name}')
|
74
|
+
end
|
75
|
+
EOS
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -3,7 +3,7 @@ module CouchRest
|
|
3
3
|
module Persistence
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
# Create the document. Validation is enabled by default and will return
|
6
|
+
# Create the document. Validation is enabled by default and will return
|
7
7
|
# false if the document is not valid. If all goes well, the document will
|
8
8
|
# be returned.
|
9
9
|
def create(options = {})
|
@@ -16,13 +16,13 @@ module CouchRest
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
# Creates the document in the db. Raises an exception
|
21
21
|
# if the document is not created properly.
|
22
22
|
def create!
|
23
23
|
self.class.fail_validate!(self) unless self.create
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
# Trigger the callbacks (before, after, around)
|
27
27
|
# only if the document isn't new
|
28
28
|
def update(options = {})
|
@@ -35,12 +35,12 @@ module CouchRest
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
# Trigger the callbacks (before, after, around) and save the document
|
40
40
|
def save(options = {})
|
41
41
|
self.new? ? create(options) : update(options)
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
# Saves the document to the db using save. Raises an exception
|
45
45
|
# if the document is not saved properly.
|
46
46
|
def save!
|
@@ -65,7 +65,6 @@ module CouchRest
|
|
65
65
|
# Update the document's attributes and save. For example:
|
66
66
|
#
|
67
67
|
# doc.update_attributes :name => "Fred"
|
68
|
-
#
|
69
68
|
# Is the equivilent of doing the following:
|
70
69
|
#
|
71
70
|
# doc.attributes = { :name => "Fred" }
|
@@ -76,7 +75,17 @@ module CouchRest
|
|
76
75
|
save
|
77
76
|
end
|
78
77
|
|
79
|
-
|
78
|
+
# Reloads the attributes of this object from the database.
|
79
|
+
# It doesn't override custom instance variables.
|
80
|
+
#
|
81
|
+
# Returns self.
|
82
|
+
def reload
|
83
|
+
merge!(self.class.get(id))
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
protected
|
80
89
|
|
81
90
|
def perform_validations(options = {})
|
82
91
|
perform_validation = case options
|
@@ -93,16 +102,16 @@ module CouchRest
|
|
93
102
|
|
94
103
|
# Creates a new instance, bypassing attribute protection
|
95
104
|
#
|
96
|
-
#
|
97
105
|
# ==== Returns
|
98
106
|
# a document instance
|
99
|
-
|
107
|
+
#
|
108
|
+
def build_from_database(doc = {})
|
100
109
|
base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
|
101
|
-
base.new(doc, :directly_set_attributes => true)
|
110
|
+
base.new(doc, :directly_set_attributes => true)
|
102
111
|
end
|
103
112
|
|
104
|
-
# Defines an instance and save it directly to the database
|
105
|
-
#
|
113
|
+
# Defines an instance and save it directly to the database
|
114
|
+
#
|
106
115
|
# ==== Returns
|
107
116
|
# returns the reloaded document
|
108
117
|
def create(attributes = {})
|
@@ -110,9 +119,9 @@ module CouchRest
|
|
110
119
|
instance.create
|
111
120
|
instance
|
112
121
|
end
|
113
|
-
|
114
|
-
# Defines an instance and save it directly to the database
|
115
|
-
#
|
122
|
+
|
123
|
+
# Defines an instance and save it directly to the database
|
124
|
+
#
|
116
125
|
# ==== Returns
|
117
126
|
# returns the reloaded document or raises an exception
|
118
127
|
def create!(attributes = {})
|
@@ -148,7 +157,7 @@ module CouchRest
|
|
148
157
|
raise Errors::Validations.new(document)
|
149
158
|
end
|
150
159
|
end
|
151
|
-
|
160
|
+
|
152
161
|
|
153
162
|
end
|
154
163
|
end
|
@@ -156,7 +156,11 @@ module CouchRest
|
|
156
156
|
type = Class.new(Hash) do
|
157
157
|
include CastedModel
|
158
158
|
end
|
159
|
-
|
159
|
+
if block.arity == 1 # Traditional, with options
|
160
|
+
type.class_eval { yield type }
|
161
|
+
else
|
162
|
+
type.instance_exec(&block)
|
163
|
+
end
|
160
164
|
type = [type] # inject as an array
|
161
165
|
end
|
162
166
|
property = Property.new(name, type, options)
|