jgre-couchrest 0.12.6
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/LICENSE +176 -0
- data/README.md +68 -0
- data/Rakefile +66 -0
- data/THANKS.md +18 -0
- data/examples/model/example.rb +138 -0
- data/examples/word_count/markov +38 -0
- data/examples/word_count/views/books/chunked-map.js +3 -0
- data/examples/word_count/views/books/united-map.js +1 -0
- data/examples/word_count/views/markov/chain-map.js +6 -0
- data/examples/word_count/views/markov/chain-reduce.js +7 -0
- data/examples/word_count/views/word_count/count-map.js +6 -0
- data/examples/word_count/views/word_count/count-reduce.js +3 -0
- data/examples/word_count/word_count.rb +46 -0
- data/examples/word_count/word_count_query.rb +40 -0
- data/examples/word_count/word_count_views.rb +26 -0
- data/lib/couchrest.rb +139 -0
- data/lib/couchrest/commands/generate.rb +71 -0
- data/lib/couchrest/commands/push.rb +103 -0
- data/lib/couchrest/core/database.rb +241 -0
- data/lib/couchrest/core/design.rb +89 -0
- data/lib/couchrest/core/document.rb +94 -0
- data/lib/couchrest/core/model.rb +613 -0
- data/lib/couchrest/core/server.rb +51 -0
- data/lib/couchrest/core/view.rb +4 -0
- data/lib/couchrest/helper/pager.rb +103 -0
- data/lib/couchrest/helper/streamer.rb +44 -0
- data/lib/couchrest/monkeypatches.rb +38 -0
- data/spec/couchrest/core/couchrest_spec.rb +201 -0
- data/spec/couchrest/core/database_spec.rb +629 -0
- data/spec/couchrest/core/design_spec.rb +131 -0
- data/spec/couchrest/core/document_spec.rb +213 -0
- data/spec/couchrest/core/model_spec.rb +859 -0
- data/spec/couchrest/helpers/pager_spec.rb +122 -0
- data/spec/couchrest/helpers/streamer_spec.rb +23 -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/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/spec.opts +6 -0
- data/spec/spec_helper.rb +20 -0
- data/utils/remap.rb +27 -0
- data/utils/subset.rb +30 -0
- metadata +143 -0
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require "base64"
|
3
|
+
|
4
|
+
module CouchRest
|
5
|
+
class Database
|
6
|
+
attr_reader :server, :host, :name, :root
|
7
|
+
attr_accessor :bulk_save_cache_limit
|
8
|
+
|
9
|
+
# Create a CouchRest::Database adapter for the supplied CouchRest::Server
|
10
|
+
# and database name.
|
11
|
+
#
|
12
|
+
# ==== Parameters
|
13
|
+
# server<CouchRest::Server>:: database host
|
14
|
+
# name<String>:: database name
|
15
|
+
#
|
16
|
+
def initialize server, name
|
17
|
+
@name = name
|
18
|
+
@server = server
|
19
|
+
@host = server.uri
|
20
|
+
@root = "#{host}/#{name}"
|
21
|
+
@streamer = Streamer.new(self)
|
22
|
+
@bulk_save_cache = []
|
23
|
+
@bulk_save_cache_limit = 50
|
24
|
+
end
|
25
|
+
|
26
|
+
# returns the database's uri
|
27
|
+
def to_s
|
28
|
+
@root
|
29
|
+
end
|
30
|
+
|
31
|
+
# GET the database info from CouchDB
|
32
|
+
def info
|
33
|
+
CouchRest.get @root
|
34
|
+
end
|
35
|
+
|
36
|
+
# Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
|
37
|
+
def documents params = {}
|
38
|
+
keys = params.delete(:keys)
|
39
|
+
url = CouchRest.paramify_url "#{@root}/_all_docs", params
|
40
|
+
if keys
|
41
|
+
CouchRest.post(url, {:keys => keys})
|
42
|
+
else
|
43
|
+
CouchRest.get url
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# POST a temporary view function to CouchDB for querying. This is not
|
48
|
+
# recommended, as you don't get any performance benefit from CouchDB's
|
49
|
+
# materialized views. Can be quite slow on large databases.
|
50
|
+
def slow_view funcs, params = {}
|
51
|
+
keys = params.delete(:keys)
|
52
|
+
funcs = funcs.merge({:keys => keys}) if keys
|
53
|
+
url = CouchRest.paramify_url "#{@root}/_temp_view", params
|
54
|
+
JSON.parse(RestClient.post(url, funcs.to_json, {"Content-Type" => 'application/json'}))
|
55
|
+
end
|
56
|
+
|
57
|
+
# backwards compatibility is a plus
|
58
|
+
alias :temp_view :slow_view
|
59
|
+
|
60
|
+
# Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
|
61
|
+
# paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
|
62
|
+
def view name, params = {}, &block
|
63
|
+
keys = params.delete(:keys)
|
64
|
+
url = CouchRest.paramify_url "#{@root}/_view/#{name}", params
|
65
|
+
if keys
|
66
|
+
CouchRest.post(url, {:keys => keys})
|
67
|
+
else
|
68
|
+
if block_given?
|
69
|
+
@streamer.view(name, params, &block)
|
70
|
+
else
|
71
|
+
CouchRest.get url
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# GET a document from CouchDB, by id. Returns a Ruby Hash.
|
77
|
+
def get id
|
78
|
+
slug = escape_docid(id)
|
79
|
+
hash = CouchRest.get("#{@root}/#{slug}")
|
80
|
+
doc = if /^_design/ =~ hash["_id"]
|
81
|
+
Design.new(hash)
|
82
|
+
else
|
83
|
+
Document.new(hash)
|
84
|
+
end
|
85
|
+
doc.database = self
|
86
|
+
doc
|
87
|
+
end
|
88
|
+
|
89
|
+
# GET an attachment directly from CouchDB
|
90
|
+
def fetch_attachment docid, name
|
91
|
+
slug = escape_docid(docid)
|
92
|
+
name = CGI.escape(name)
|
93
|
+
RestClient.get "#{@root}/#{slug}/#{name}"
|
94
|
+
end
|
95
|
+
|
96
|
+
# PUT an attachment directly to CouchDB
|
97
|
+
def put_attachment doc, name, file, options = {}
|
98
|
+
docid = escape_docid(doc['_id'])
|
99
|
+
name = CGI.escape(name)
|
100
|
+
uri = if doc['_rev']
|
101
|
+
"#{@root}/#{docid}/#{name}?rev=#{doc['_rev']}"
|
102
|
+
else
|
103
|
+
"#{@root}/#{docid}/#{name}"
|
104
|
+
end
|
105
|
+
|
106
|
+
JSON.parse(RestClient.put(uri, file, options))
|
107
|
+
end
|
108
|
+
|
109
|
+
# Save a document to CouchDB. This will use the <tt>_id</tt> field from
|
110
|
+
# the document as the id for PUT, or request a new UUID from CouchDB, if
|
111
|
+
# no <tt>_id</tt> is present on the document. IDs are attached to
|
112
|
+
# documents on the client side because POST has the curious property of
|
113
|
+
# being automatically retried by proxies in the event of network
|
114
|
+
# segmentation and lost responses.
|
115
|
+
#
|
116
|
+
# If <tt>bulk</tt> is true (false by default) the document is cached for bulk-saving later.
|
117
|
+
# Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
|
118
|
+
def save (doc, bulk = false)
|
119
|
+
if doc['_attachments']
|
120
|
+
doc['_attachments'] = encode_attachments(doc['_attachments'])
|
121
|
+
end
|
122
|
+
if bulk
|
123
|
+
@bulk_save_cache << doc
|
124
|
+
return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
|
125
|
+
return {"ok" => true} # Compatibility with Document#save
|
126
|
+
elsif !bulk && @bulk_save_cache.length > 0
|
127
|
+
bulk_save
|
128
|
+
end
|
129
|
+
result = if doc['_id']
|
130
|
+
slug = escape_docid(doc['_id'])
|
131
|
+
CouchRest.put "#{@root}/#{slug}", doc
|
132
|
+
else
|
133
|
+
begin
|
134
|
+
slug = doc['_id'] = @server.next_uuid
|
135
|
+
CouchRest.put "#{@root}/#{slug}", doc
|
136
|
+
rescue #old version of couchdb
|
137
|
+
CouchRest.post @root, doc
|
138
|
+
end
|
139
|
+
end
|
140
|
+
if result['ok']
|
141
|
+
doc['_id'] = result['id']
|
142
|
+
doc['_rev'] = result['rev']
|
143
|
+
doc.database = self if doc.respond_to?(:database=)
|
144
|
+
end
|
145
|
+
result
|
146
|
+
end
|
147
|
+
|
148
|
+
# POST an array of documents to CouchDB. If any of the documents are
|
149
|
+
# missing ids, supply one from the uuid cache.
|
150
|
+
#
|
151
|
+
# If called with no arguments, bulk saves the cache of documents to be bulk saved.
|
152
|
+
def bulk_save(docs = nil, use_uuids = true)
|
153
|
+
if docs.nil?
|
154
|
+
docs = @bulk_save_cache
|
155
|
+
@bulk_save_cache = []
|
156
|
+
end
|
157
|
+
if (use_uuids)
|
158
|
+
ids, noids = docs.partition{|d|d['_id']}
|
159
|
+
uuid_count = [noids.length, @server.uuid_batch_count].max
|
160
|
+
noids.each do |doc|
|
161
|
+
nextid = @server.next_uuid(uuid_count) rescue nil
|
162
|
+
doc['_id'] = nextid if nextid
|
163
|
+
end
|
164
|
+
end
|
165
|
+
CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
|
166
|
+
end
|
167
|
+
|
168
|
+
# DELETE the document from CouchDB that has the given <tt>_id</tt> and
|
169
|
+
# <tt>_rev</tt>.
|
170
|
+
#
|
171
|
+
# If <tt>bulk</tt> is true (false by default) the deletion is recorded for bulk-saving (bulk-deletion :) later.
|
172
|
+
# Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
|
173
|
+
def delete (doc, bulk = false)
|
174
|
+
raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev']
|
175
|
+
if bulk
|
176
|
+
@bulk_save_cache << { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true }
|
177
|
+
return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
|
178
|
+
return { "ok" => true } # Mimic the non-deferred version
|
179
|
+
end
|
180
|
+
slug = escape_docid(doc['_id'])
|
181
|
+
CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
|
182
|
+
end
|
183
|
+
|
184
|
+
# COPY an existing document to a new id. If the destination id currently exists, a rev must be provided.
|
185
|
+
# <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
|
186
|
+
# hash with a '_rev' key
|
187
|
+
def copy doc, dest
|
188
|
+
raise ArgumentError, "_id is required for copying" unless doc['_id']
|
189
|
+
slug = escape_docid(doc['_id'])
|
190
|
+
destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev']
|
191
|
+
"#{dest['_id']}?rev=#{dest['_rev']}"
|
192
|
+
else
|
193
|
+
dest
|
194
|
+
end
|
195
|
+
CouchRest.copy "#{@root}/#{slug}", destination
|
196
|
+
end
|
197
|
+
|
198
|
+
# MOVE an existing document to a new id. If the destination id currently exists, a rev must be provided.
|
199
|
+
# <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
|
200
|
+
# hash with a '_rev' key
|
201
|
+
def move doc, dest
|
202
|
+
raise ArgumentError, "_id and _rev are required for moving" unless doc['_id'] && doc['_rev']
|
203
|
+
slug = escape_docid(doc['_id'])
|
204
|
+
destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev']
|
205
|
+
"#{dest['_id']}?rev=#{dest['_rev']}"
|
206
|
+
else
|
207
|
+
dest
|
208
|
+
end
|
209
|
+
CouchRest.move "#{@root}/#{slug}?rev=#{doc['_rev']}", destination
|
210
|
+
end
|
211
|
+
|
212
|
+
# Compact the database, removing old document revisions and optimizing space use.
|
213
|
+
def compact!
|
214
|
+
CouchRest.post "#{@root}/_compact"
|
215
|
+
end
|
216
|
+
|
217
|
+
# DELETE the database itself. This is not undoable and could be rather
|
218
|
+
# catastrophic. Use with care!
|
219
|
+
def delete!
|
220
|
+
CouchRest.delete @root
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
def escape_docid id
|
226
|
+
/^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
|
227
|
+
end
|
228
|
+
|
229
|
+
def encode_attachments attachments
|
230
|
+
attachments.each do |k,v|
|
231
|
+
next if v['stub']
|
232
|
+
v['data'] = base64(v['data'])
|
233
|
+
end
|
234
|
+
attachments
|
235
|
+
end
|
236
|
+
|
237
|
+
def base64 data
|
238
|
+
Base64.encode64(data).gsub(/\s/,'')
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module CouchRest
|
2
|
+
class Design < Document
|
3
|
+
def view_by *keys
|
4
|
+
opts = keys.pop if keys.last.is_a?(Hash)
|
5
|
+
opts ||= {}
|
6
|
+
self['views'] ||= {}
|
7
|
+
method_name = "by_#{keys.join('_and_')}"
|
8
|
+
|
9
|
+
if opts[:map]
|
10
|
+
view = {}
|
11
|
+
view['map'] = opts.delete(:map)
|
12
|
+
if opts[:reduce]
|
13
|
+
view['reduce'] = opts.delete(:reduce)
|
14
|
+
opts[:reduce] = false
|
15
|
+
end
|
16
|
+
self['views'][method_name] = view
|
17
|
+
else
|
18
|
+
doc_keys = keys.collect{|k|"doc['#{k}']"} # this is where :require => 'doc.x == true' would show up
|
19
|
+
key_emit = doc_keys.length == 1 ? "#{doc_keys.first}" : "[#{doc_keys.join(', ')}]"
|
20
|
+
guards = opts.delete(:guards) || []
|
21
|
+
guards.concat doc_keys
|
22
|
+
map_function = <<-JAVASCRIPT
|
23
|
+
function(doc) {
|
24
|
+
if (#{guards.join(' && ')}) {
|
25
|
+
emit(#{key_emit}, null);
|
26
|
+
}
|
27
|
+
}
|
28
|
+
JAVASCRIPT
|
29
|
+
self['views'][method_name] = {
|
30
|
+
'map' => map_function
|
31
|
+
}
|
32
|
+
end
|
33
|
+
self['views'][method_name]['couchrest-defaults'] = opts unless opts.empty?
|
34
|
+
method_name
|
35
|
+
end
|
36
|
+
|
37
|
+
# Dispatches to any named view.
|
38
|
+
def view view_name, query={}, &block
|
39
|
+
view_name = view_name.to_s
|
40
|
+
view_slug = "#{name}/#{view_name}"
|
41
|
+
defaults = (self['views'][view_name] && self['views'][view_name]["couchrest-defaults"]) || {}
|
42
|
+
fetch_view(view_slug, defaults.merge(query), &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def name
|
46
|
+
id.sub('_design/','') if id
|
47
|
+
end
|
48
|
+
|
49
|
+
def name= newname
|
50
|
+
self['_id'] = "_design/#{newname}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def save
|
54
|
+
raise ArgumentError, "_design docs require a name" unless name && name.length > 0
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# returns stored defaults if the there is a view named this in the design doc
|
61
|
+
def has_view?(view)
|
62
|
+
view = view.to_s
|
63
|
+
self['views'][view] &&
|
64
|
+
(self['views'][view]["couchrest-defaults"]||{})
|
65
|
+
end
|
66
|
+
|
67
|
+
# def fetch_view_with_docs name, opts, raw=false, &block
|
68
|
+
# if raw
|
69
|
+
# fetch_view name, opts, &block
|
70
|
+
# else
|
71
|
+
# begin
|
72
|
+
# view = fetch_view name, opts.merge({:include_docs => true}), &block
|
73
|
+
# view['rows'].collect{|r|new(r['doc'])} if view['rows']
|
74
|
+
# rescue
|
75
|
+
# # fallback for old versions of couchdb that don't
|
76
|
+
# # have include_docs support
|
77
|
+
# view = fetch_view name, opts, &block
|
78
|
+
# view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
|
79
|
+
# end
|
80
|
+
# end
|
81
|
+
# end
|
82
|
+
|
83
|
+
def fetch_view view_name, opts, &block
|
84
|
+
database.view(view_name, opts, &block)
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module CouchRest
|
2
|
+
class Response
|
3
|
+
def initialize keys = {}
|
4
|
+
@hash = {}
|
5
|
+
keys.each do |k,v|
|
6
|
+
self[k.to_s] = v
|
7
|
+
end
|
8
|
+
end
|
9
|
+
attr_reader :hash
|
10
|
+
def ==(other)
|
11
|
+
@hash == other.hash
|
12
|
+
end
|
13
|
+
def each(&blk)
|
14
|
+
@hash.each(&blk)
|
15
|
+
end
|
16
|
+
def []= key, value
|
17
|
+
@hash[key.to_s] = value
|
18
|
+
end
|
19
|
+
def [] key
|
20
|
+
@hash[key.to_s]
|
21
|
+
end
|
22
|
+
def to_json
|
23
|
+
@hash.to_json
|
24
|
+
end
|
25
|
+
def delete key
|
26
|
+
@hash.delete key
|
27
|
+
end
|
28
|
+
def key? key
|
29
|
+
@hash.key? key
|
30
|
+
end
|
31
|
+
def has_key? key
|
32
|
+
@hash.has_key key
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Document < Response
|
37
|
+
|
38
|
+
attr_accessor :database
|
39
|
+
|
40
|
+
# alias for self['_id']
|
41
|
+
def id
|
42
|
+
self['_id']
|
43
|
+
end
|
44
|
+
|
45
|
+
# alias for self['_rev']
|
46
|
+
def rev
|
47
|
+
self['_rev']
|
48
|
+
end
|
49
|
+
|
50
|
+
# returns true if the document has never been saved
|
51
|
+
def new_document?
|
52
|
+
!rev
|
53
|
+
end
|
54
|
+
|
55
|
+
# Saves the document to the db using create or update. Also runs the :save
|
56
|
+
# callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on
|
57
|
+
# CouchDB's response.
|
58
|
+
# If <tt>bulk</tt> is <tt>true</tt> (defaults to false) the document is cached for bulk save.
|
59
|
+
def save(bulk = false)
|
60
|
+
raise ArgumentError, "doc.database required for saving" unless database
|
61
|
+
result = database.save @hash, bulk
|
62
|
+
result['ok']
|
63
|
+
end
|
64
|
+
|
65
|
+
# Deletes the document from the database. Runs the :delete callbacks.
|
66
|
+
# Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
|
67
|
+
# document to be saved to a new <tt>_id</tt>.
|
68
|
+
# If <tt>bulk</tt> is <tt>true</tt> (defaults to false) the document won't
|
69
|
+
# actually be deleted from the db until bulk save.
|
70
|
+
def destroy(bulk = false)
|
71
|
+
raise ArgumentError, "doc.database required to destroy" unless database
|
72
|
+
result = database.delete(@hash, bulk)
|
73
|
+
if result['ok']
|
74
|
+
self['_rev'] = nil
|
75
|
+
self['_id'] = nil
|
76
|
+
end
|
77
|
+
result['ok']
|
78
|
+
end
|
79
|
+
|
80
|
+
def copy(dest)
|
81
|
+
raise ArgumentError, "doc.database required to copy" unless database
|
82
|
+
result = database.copy(@hash, dest)
|
83
|
+
result['ok']
|
84
|
+
end
|
85
|
+
|
86
|
+
def move(dest)
|
87
|
+
raise ArgumentError, "doc.database required to copy" unless database
|
88
|
+
result = database.move(@hash, dest)
|
89
|
+
result['ok']
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
@@ -0,0 +1,613 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
begin
|
3
|
+
require 'extlib'
|
4
|
+
rescue
|
5
|
+
puts "CouchRest::Model requires extlib. This is left out of the gemspec on purpose."
|
6
|
+
raise
|
7
|
+
end
|
8
|
+
require 'digest/md5'
|
9
|
+
require File.dirname(__FILE__) + '/document'
|
10
|
+
require 'mime/types'
|
11
|
+
|
12
|
+
# = CouchRest::Model - Document modeling, the CouchDB way
|
13
|
+
module CouchRest
|
14
|
+
# = CouchRest::Model - Document modeling, the CouchDB way
|
15
|
+
#
|
16
|
+
# CouchRest::Model provides an ORM-like interface for CouchDB documents. It
|
17
|
+
# avoids all usage of <tt>method_missing</tt>, and tries to strike a balance
|
18
|
+
# between usability and magic. See CouchRest::Model#view_by for
|
19
|
+
# documentation about the view-generation system.
|
20
|
+
#
|
21
|
+
# ==== Example
|
22
|
+
#
|
23
|
+
# This is an example class using CouchRest::Model. It is taken from the
|
24
|
+
# spec/couchrest/core/model_spec.rb file, which may be even more up to date
|
25
|
+
# than this example.
|
26
|
+
#
|
27
|
+
# class Article < CouchRest::Model
|
28
|
+
# use_database CouchRest.database!('http://127.0.0.1:5984/couchrest-model-test')
|
29
|
+
# unique_id :slug
|
30
|
+
#
|
31
|
+
# view_by :date, :descending => true
|
32
|
+
# view_by :user_id, :date
|
33
|
+
#
|
34
|
+
# view_by :tags,
|
35
|
+
# :map =>
|
36
|
+
# "function(doc) {
|
37
|
+
# if (doc['couchrest-type'] == 'Article' && doc.tags) {
|
38
|
+
# doc.tags.forEach(function(tag){
|
39
|
+
# emit(tag, 1);
|
40
|
+
# });
|
41
|
+
# }
|
42
|
+
# }",
|
43
|
+
# :reduce =>
|
44
|
+
# "function(keys, values, rereduce) {
|
45
|
+
# return sum(values);
|
46
|
+
# }"
|
47
|
+
#
|
48
|
+
# key_writer :date
|
49
|
+
# key_reader :slug, :created_at, :updated_at
|
50
|
+
# key_accessor :title, :tags
|
51
|
+
#
|
52
|
+
# timestamps!
|
53
|
+
#
|
54
|
+
# before(:create, :generate_slug_from_title)
|
55
|
+
# def generate_slug_from_title
|
56
|
+
# self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# ==== Examples of finding articles with these views:
|
61
|
+
#
|
62
|
+
# * All the articles by Barney published in the last 24 hours. Note that we
|
63
|
+
# use <tt>{}</tt> as a special value that sorts after all strings,
|
64
|
+
# numbers, and arrays.
|
65
|
+
#
|
66
|
+
# Article.by_user_id_and_date :startkey => ["barney", Time.now - 24 * 3600], :endkey => ["barney", {}]
|
67
|
+
#
|
68
|
+
# * The most recent 20 articles. Remember that the <tt>view_by :date</tt>
|
69
|
+
# has the default option <tt>:descending => true</tt>.
|
70
|
+
#
|
71
|
+
# Article.by_date :limit => 20
|
72
|
+
#
|
73
|
+
# * The raw CouchDB view reduce result for the custom <tt>:tags</tt> view.
|
74
|
+
# In this case we'll get a count of the number of articles tagged "ruby".
|
75
|
+
#
|
76
|
+
# Article.by_tags :key => "ruby", :reduce => true
|
77
|
+
#
|
78
|
+
class Model < Document
|
79
|
+
|
80
|
+
# instantiates the hash by converting all the keys to strings.
|
81
|
+
def initialize keys = {}
|
82
|
+
super(keys)
|
83
|
+
apply_defaults
|
84
|
+
cast_keys
|
85
|
+
unless self['_id'] && self['_rev']
|
86
|
+
self['couchrest-type'] = self.class.to_s
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# this is the CouchRest::Database that model classes will use unless
|
91
|
+
# they override it with <tt>use_database</tt>
|
92
|
+
cattr_accessor :default_database
|
93
|
+
|
94
|
+
class_inheritable_accessor :casts
|
95
|
+
class_inheritable_accessor :default_obj
|
96
|
+
class_inheritable_accessor :class_database
|
97
|
+
class_inheritable_accessor :design_doc
|
98
|
+
class_inheritable_accessor :design_doc_slug_cache
|
99
|
+
class_inheritable_accessor :design_doc_fresh
|
100
|
+
|
101
|
+
class << self
|
102
|
+
# override the CouchRest::Model-wide default_database
|
103
|
+
def use_database db
|
104
|
+
self.class_database = db
|
105
|
+
end
|
106
|
+
|
107
|
+
# returns the CouchRest::Database instance that this class uses
|
108
|
+
def database
|
109
|
+
self.class_database || CouchRest::Model.default_database
|
110
|
+
end
|
111
|
+
|
112
|
+
# Load a document from the database by id
|
113
|
+
def get id
|
114
|
+
doc = database.get id
|
115
|
+
new(doc)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Load all documents that have the "couchrest-type" field equal to the
|
119
|
+
# name of the current class. Take the standard set of
|
120
|
+
# CouchRest::Database#view options.
|
121
|
+
def all opts = {}, &block
|
122
|
+
self.design_doc ||= Design.new(default_design_doc)
|
123
|
+
unless design_doc_fresh
|
124
|
+
refresh_design_doc
|
125
|
+
end
|
126
|
+
view :all, opts, &block
|
127
|
+
end
|
128
|
+
|
129
|
+
# Load the first document that have the "couchrest-type" field equal to
|
130
|
+
# the name of the current class.
|
131
|
+
#
|
132
|
+
# ==== Returns
|
133
|
+
# Object:: The first object instance available
|
134
|
+
# or
|
135
|
+
# Nil:: if no instances available
|
136
|
+
#
|
137
|
+
# ==== Parameters
|
138
|
+
# opts<Hash>::
|
139
|
+
# View options, see <tt>CouchRest::Database#view</tt> options for more info.
|
140
|
+
def first opts = {}
|
141
|
+
first_instance = self.all(opts.merge!(:limit => 1))
|
142
|
+
first_instance.empty? ? nil : first_instance.first
|
143
|
+
end
|
144
|
+
|
145
|
+
# Cast a field as another class. The class must be happy to have the
|
146
|
+
# field's primitive type as the argument to it's constuctur. Classes
|
147
|
+
# which inherit from CouchRest::Model are happy to act as sub-objects
|
148
|
+
# for any fields that are stored in JSON as object (and therefore are
|
149
|
+
# parsed from the JSON as Ruby Hashes).
|
150
|
+
#
|
151
|
+
# Example:
|
152
|
+
#
|
153
|
+
# class Post < CouchRest::Model
|
154
|
+
#
|
155
|
+
# key_accessor :title, :body, :author
|
156
|
+
#
|
157
|
+
# cast :author, :as => 'Author'
|
158
|
+
#
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# post.author.class #=> Author
|
162
|
+
#
|
163
|
+
# Using the same example, if a Post should have many Comments, we
|
164
|
+
# would declare it like this:
|
165
|
+
#
|
166
|
+
# class Post < CouchRest::Model
|
167
|
+
#
|
168
|
+
# key_accessor :title, :body, :author, comments
|
169
|
+
#
|
170
|
+
# cast :author, :as => 'Author'
|
171
|
+
# cast :comments, :as => ['Comment']
|
172
|
+
#
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
# post.author.class #=> Author
|
176
|
+
# post.comments.class #=> Array
|
177
|
+
# post.comments.first #=> Comment
|
178
|
+
#
|
179
|
+
def cast field, opts = {}
|
180
|
+
self.casts ||= {}
|
181
|
+
self.casts[field.to_s] = opts
|
182
|
+
end
|
183
|
+
|
184
|
+
# Defines methods for reading and writing from fields in the document.
|
185
|
+
# Uses key_writer and key_reader internally.
|
186
|
+
def key_accessor *keys
|
187
|
+
key_writer *keys
|
188
|
+
key_reader *keys
|
189
|
+
end
|
190
|
+
|
191
|
+
# For each argument key, define a method <tt>key=</tt> that sets the
|
192
|
+
# corresponding field on the CouchDB document.
|
193
|
+
def key_writer *keys
|
194
|
+
keys.each do |method|
|
195
|
+
key = method.to_s
|
196
|
+
define_method "#{method}=" do |value|
|
197
|
+
self[key] = value
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# For each argument key, define a method <tt>key</tt> that reads the
|
203
|
+
# corresponding field on the CouchDB document.
|
204
|
+
def key_reader *keys
|
205
|
+
keys.each do |method|
|
206
|
+
key = method.to_s
|
207
|
+
define_method method do
|
208
|
+
self[key]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def default
|
214
|
+
self.default_obj
|
215
|
+
end
|
216
|
+
|
217
|
+
def set_default hash
|
218
|
+
self.default_obj = hash
|
219
|
+
end
|
220
|
+
|
221
|
+
# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
|
222
|
+
# on the document whenever saving occurs. CouchRest uses a pretty
|
223
|
+
# decent time format by default. See Time#to_json
|
224
|
+
def timestamps!
|
225
|
+
before(:save) do
|
226
|
+
self['updated_at'] = Time.now
|
227
|
+
self['created_at'] = self['updated_at'] if new_document?
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Name a method that will be called before the document is first saved,
|
232
|
+
# which returns a string to be used for the document's <tt>_id</tt>.
|
233
|
+
# Because CouchDB enforces a constraint that each id must be unique,
|
234
|
+
# this can be used to enforce eg: uniq usernames. Note that this id
|
235
|
+
# must be globally unique across all document types which share a
|
236
|
+
# database, so if you'd like to scope uniqueness to this class, you
|
237
|
+
# should use the class name as part of the unique id.
|
238
|
+
def unique_id method = nil, &block
|
239
|
+
if method
|
240
|
+
define_method :set_unique_id do
|
241
|
+
self['_id'] ||= self.send(method)
|
242
|
+
end
|
243
|
+
elsif block
|
244
|
+
define_method :set_unique_id do
|
245
|
+
uniqid = block.call(self)
|
246
|
+
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
|
247
|
+
self['_id'] ||= uniqid
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Define a CouchDB view. The name of the view will be the concatenation
|
253
|
+
# of <tt>by</tt> and the keys joined by <tt>_and_</tt>
|
254
|
+
#
|
255
|
+
# ==== Example views:
|
256
|
+
#
|
257
|
+
# class Post
|
258
|
+
# # view with default options
|
259
|
+
# # query with Post.by_date
|
260
|
+
# view_by :date, :descending => true
|
261
|
+
#
|
262
|
+
# # view with compound sort-keys
|
263
|
+
# # query with Post.by_user_id_and_date
|
264
|
+
# view_by :user_id, :date
|
265
|
+
#
|
266
|
+
# # view with custom map/reduce functions
|
267
|
+
# # query with Post.by_tags :reduce => true
|
268
|
+
# view_by :tags,
|
269
|
+
# :map =>
|
270
|
+
# "function(doc) {
|
271
|
+
# if (doc['couchrest-type'] == 'Post' && doc.tags) {
|
272
|
+
# doc.tags.forEach(function(tag){
|
273
|
+
# emit(doc.tag, 1);
|
274
|
+
# });
|
275
|
+
# }
|
276
|
+
# }",
|
277
|
+
# :reduce =>
|
278
|
+
# "function(keys, values, rereduce) {
|
279
|
+
# return sum(values);
|
280
|
+
# }"
|
281
|
+
# end
|
282
|
+
#
|
283
|
+
# <tt>view_by :date</tt> will create a view defined by this Javascript
|
284
|
+
# function:
|
285
|
+
#
|
286
|
+
# function(doc) {
|
287
|
+
# if (doc['couchrest-type'] == 'Post' && doc.date) {
|
288
|
+
# emit(doc.date, null);
|
289
|
+
# }
|
290
|
+
# }
|
291
|
+
#
|
292
|
+
# It can be queried by calling <tt>Post.by_date</tt> which accepts all
|
293
|
+
# valid options for CouchRest::Database#view. In addition, calling with
|
294
|
+
# the <tt>:raw => true</tt> option will return the view rows
|
295
|
+
# themselves. By default <tt>Post.by_date</tt> will return the
|
296
|
+
# documents included in the generated view.
|
297
|
+
#
|
298
|
+
# CouchRest::Database#view options can be applied at view definition
|
299
|
+
# time as defaults, and they will be curried and used at view query
|
300
|
+
# time. Or they can be overridden at query time.
|
301
|
+
#
|
302
|
+
# Custom views can be queried with <tt>:reduce => true</tt> to return
|
303
|
+
# reduce results. The default for custom views is to query with
|
304
|
+
# <tt>:reduce => false</tt>.
|
305
|
+
#
|
306
|
+
# Views are generated (on a per-model basis) lazily on first-access.
|
307
|
+
# This means that if you are deploying changes to a view, the views for
|
308
|
+
# that model won't be available until generation is complete. This can
|
309
|
+
# take some time with large databases. Strategies are in the works.
|
310
|
+
#
|
311
|
+
# To understand the capabilities of this view system more compeletly,
|
312
|
+
# it is recommended that you read the RSpec file at
|
313
|
+
# <tt>spec/core/model_spec.rb</tt>.
|
314
|
+
|
315
|
+
def view_by *keys
|
316
|
+
self.design_doc ||= Design.new(default_design_doc)
|
317
|
+
opts = keys.pop if keys.last.is_a?(Hash)
|
318
|
+
opts ||= {}
|
319
|
+
ducktype = opts.delete(:ducktype)
|
320
|
+
unless ducktype || opts[:map]
|
321
|
+
opts[:guards] ||= []
|
322
|
+
opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
|
323
|
+
end
|
324
|
+
keys.push opts
|
325
|
+
self.design_doc.view_by(*keys)
|
326
|
+
self.design_doc_fresh = false
|
327
|
+
end
|
328
|
+
|
329
|
+
def method_missing m, *args
|
330
|
+
if has_view?(m)
|
331
|
+
query = args.shift || {}
|
332
|
+
view(m, query, *args)
|
333
|
+
else
|
334
|
+
super
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# returns stored defaults if the there is a view named this in the design doc
|
339
|
+
def has_view?(view)
|
340
|
+
view = view.to_s
|
341
|
+
design_doc && design_doc['views'] && design_doc['views'][view]
|
342
|
+
end
|
343
|
+
|
344
|
+
# Dispatches to any named view.
|
345
|
+
def view name, query={}, &block
|
346
|
+
unless design_doc_fresh
|
347
|
+
refresh_design_doc
|
348
|
+
end
|
349
|
+
query[:raw] = true if query[:reduce]
|
350
|
+
raw = query.delete(:raw)
|
351
|
+
fetch_view_with_docs(name, query, raw, &block)
|
352
|
+
end
|
353
|
+
|
354
|
+
def all_design_doc_versions
|
355
|
+
database.documents :startkey => "_design/#{self.to_s}-",
|
356
|
+
:endkey => "_design/#{self.to_s}-\u9999"
|
357
|
+
end
|
358
|
+
|
359
|
+
# Deletes any non-current design docs that were created by this class.
|
360
|
+
# Running this when you're deployed version of your application is steadily
|
361
|
+
# and consistently using the latest code, is the way to clear out old design
|
362
|
+
# docs. Running it to early could mean that live code has to regenerate
|
363
|
+
# potentially large indexes.
|
364
|
+
def cleanup_design_docs!
|
365
|
+
ddocs = all_design_doc_versions
|
366
|
+
ddocs["rows"].each do |row|
|
367
|
+
if (row['id'] != design_doc_id)
|
368
|
+
database.delete({
|
369
|
+
"_id" => row['id'],
|
370
|
+
"_rev" => row['value']['rev']
|
371
|
+
})
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
private
|
377
|
+
|
378
|
+
def fetch_view_with_docs name, opts, raw=false, &block
|
379
|
+
if raw
|
380
|
+
fetch_view name, opts, &block
|
381
|
+
else
|
382
|
+
begin
|
383
|
+
view = fetch_view name, opts.merge({:include_docs => true}), &block
|
384
|
+
view['rows'].collect{|r|new(r['doc'])} if view['rows']
|
385
|
+
rescue
|
386
|
+
# fallback for old versions of couchdb that don't
|
387
|
+
# have include_docs support
|
388
|
+
view = fetch_view name, opts, &block
|
389
|
+
view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def fetch_view view_name, opts, &block
|
395
|
+
retryable = true
|
396
|
+
begin
|
397
|
+
design_doc.view(view_name, opts, &block)
|
398
|
+
# the design doc could have been deleted by a rouge process
|
399
|
+
rescue RestClient::ResourceNotFound => e
|
400
|
+
if retryable
|
401
|
+
refresh_design_doc
|
402
|
+
retryable = false
|
403
|
+
retry
|
404
|
+
else
|
405
|
+
raise e
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def design_doc_id
|
411
|
+
"_design/#{design_doc_slug}"
|
412
|
+
end
|
413
|
+
|
414
|
+
def design_doc_slug
|
415
|
+
return design_doc_slug_cache if design_doc_slug_cache && design_doc_fresh
|
416
|
+
funcs = []
|
417
|
+
design_doc['views'].each do |name, view|
|
418
|
+
funcs << "#{name}/#{view['map']}#{view['reduce']}"
|
419
|
+
end
|
420
|
+
md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
|
421
|
+
self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
|
422
|
+
end
|
423
|
+
|
424
|
+
def default_design_doc
|
425
|
+
{
|
426
|
+
"language" => "javascript",
|
427
|
+
"views" => {
|
428
|
+
'all' => {
|
429
|
+
'map' => "function(doc) {
|
430
|
+
if (doc['couchrest-type'] == '#{self.to_s}') {
|
431
|
+
emit(null,null);
|
432
|
+
}
|
433
|
+
}"
|
434
|
+
}
|
435
|
+
}
|
436
|
+
}
|
437
|
+
end
|
438
|
+
|
439
|
+
def refresh_design_doc
|
440
|
+
did = design_doc_id
|
441
|
+
saved = database.get(did) rescue nil
|
442
|
+
if saved
|
443
|
+
design_doc['views'].each do |name, view|
|
444
|
+
saved['views'][name] = view
|
445
|
+
end
|
446
|
+
database.save(saved)
|
447
|
+
self.design_doc = saved
|
448
|
+
else
|
449
|
+
design_doc['_id'] = did
|
450
|
+
design_doc.delete('_rev')
|
451
|
+
design_doc.database = database
|
452
|
+
design_doc.save
|
453
|
+
end
|
454
|
+
self.design_doc_fresh = true
|
455
|
+
end
|
456
|
+
|
457
|
+
end # class << self
|
458
|
+
|
459
|
+
# returns the database used by this model's class
|
460
|
+
def database
|
461
|
+
self.class.database
|
462
|
+
end
|
463
|
+
|
464
|
+
# Takes a hash as argument, and applies the values by using writer methods
|
465
|
+
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
|
466
|
+
# missing. In case of error, no attributes are changed.
|
467
|
+
def update_attributes_without_saving hash
|
468
|
+
hash.each do |k, v|
|
469
|
+
raise NoMethodError, "#{k}= method not available, use key_accessor or key_writer :#{k}" unless self.respond_to?("#{k}=")
|
470
|
+
end
|
471
|
+
hash.each do |k, v|
|
472
|
+
self.send("#{k}=",v)
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
# Takes a hash as argument, and applies the values by using writer methods
|
477
|
+
# for each key. Raises a NoMethodError if the corresponding methods are
|
478
|
+
# missing. In case of error, no attributes are changed.
|
479
|
+
def update_attributes hash
|
480
|
+
update_attributes_without_saving hash
|
481
|
+
save
|
482
|
+
end
|
483
|
+
|
484
|
+
# for compatibility with old-school frameworks
|
485
|
+
alias :new_record? :new_document?
|
486
|
+
|
487
|
+
# Overridden to set the unique ID.
|
488
|
+
def save bulk = false
|
489
|
+
set_unique_id if new_document? && self.respond_to?(:set_unique_id)
|
490
|
+
super(bulk)
|
491
|
+
end
|
492
|
+
|
493
|
+
# Saves the document to the db using create or update. Raises an exception
|
494
|
+
# if the document is not saved properly.
|
495
|
+
def save!
|
496
|
+
raise "#{self.inspect} failed to save" unless self.save
|
497
|
+
end
|
498
|
+
|
499
|
+
# Deletes the document from the database. Runs the :destroy callbacks.
|
500
|
+
# Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
|
501
|
+
# document to be saved to a new <tt>_id</tt>.
|
502
|
+
def destroy
|
503
|
+
result = database.delete self
|
504
|
+
if result['ok']
|
505
|
+
self['_rev'] = nil
|
506
|
+
self['_id'] = nil
|
507
|
+
end
|
508
|
+
result['ok']
|
509
|
+
end
|
510
|
+
|
511
|
+
# creates a file attachment to the current doc
|
512
|
+
def create_attachment(args={})
|
513
|
+
raise ArgumentError unless args[:file] && args[:name]
|
514
|
+
return if has_attachment?(args[:name])
|
515
|
+
self['_attachments'] ||= {}
|
516
|
+
set_attachment_attr(args)
|
517
|
+
rescue ArgumentError => e
|
518
|
+
raise ArgumentError, 'You must specify :file and :name'
|
519
|
+
end
|
520
|
+
|
521
|
+
# reads the data from an attachment
|
522
|
+
def read_attachment(attachment_name)
|
523
|
+
Base64.decode64(database.fetch_attachment(self.id, attachment_name))
|
524
|
+
end
|
525
|
+
|
526
|
+
# modifies a file attachment on the current doc
|
527
|
+
def update_attachment(args={})
|
528
|
+
raise ArgumentError unless args[:file] && args[:name]
|
529
|
+
return unless has_attachment?(args[:name])
|
530
|
+
delete_attachment(args[:name])
|
531
|
+
set_attachment_attr(args)
|
532
|
+
rescue ArgumentError => e
|
533
|
+
raise ArgumentError, 'You must specify :file and :name'
|
534
|
+
end
|
535
|
+
|
536
|
+
# deletes a file attachment from the current doc
|
537
|
+
def delete_attachment(attachment_name)
|
538
|
+
return unless self['_attachments']
|
539
|
+
self['_attachments'].delete attachment_name
|
540
|
+
end
|
541
|
+
|
542
|
+
# returns true if attachment_name exists
|
543
|
+
def has_attachment?(attachment_name)
|
544
|
+
!!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
|
545
|
+
end
|
546
|
+
|
547
|
+
# returns URL to fetch the attachment from
|
548
|
+
def attachment_url(attachment_name)
|
549
|
+
return unless has_attachment?(attachment_name)
|
550
|
+
"#{database.root}/#{self.id}/#{attachment_name}"
|
551
|
+
end
|
552
|
+
|
553
|
+
private
|
554
|
+
|
555
|
+
def apply_defaults
|
556
|
+
return unless new_document?
|
557
|
+
if self.class.default
|
558
|
+
self.class.default.each do |k,v|
|
559
|
+
unless self.key?(k.to_s)
|
560
|
+
if v.class == Proc
|
561
|
+
self[k.to_s] = v.call
|
562
|
+
else
|
563
|
+
self[k.to_s] = Marshal.load(Marshal.dump(v))
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
def cast_keys
|
571
|
+
return unless self.class.casts
|
572
|
+
# TODO move the argument checking to the cast method for early crashes
|
573
|
+
self.class.casts.each do |k,v|
|
574
|
+
next unless self[k]
|
575
|
+
target = v[:as]
|
576
|
+
v[:send] || 'new'
|
577
|
+
if target.is_a?(Array)
|
578
|
+
klass = ::Extlib::Inflection.constantize(target[0])
|
579
|
+
self[k] = self[k].collect do |value|
|
580
|
+
(!v[:send] && klass == Time) ? Time.parse(value) : klass.send((v[:send] || 'new'), value)
|
581
|
+
end
|
582
|
+
else
|
583
|
+
self[k] = if (!v[:send] && target == 'Time')
|
584
|
+
Time.parse(self[k])
|
585
|
+
else
|
586
|
+
::Extlib::Inflection.constantize(target).send((v[:send] || 'new'), self[k])
|
587
|
+
end
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
def encode_attachment(data)
|
593
|
+
Base64.encode64(data).gsub(/\r|\n/,'')
|
594
|
+
end
|
595
|
+
|
596
|
+
def get_mime_type(file)
|
597
|
+
MIME::Types.type_for(file.path).empty? ?
|
598
|
+
'text\/plain' : MIME::Types.type_for(file.path).first.content_type.gsub(/\//,'\/')
|
599
|
+
end
|
600
|
+
|
601
|
+
def set_attachment_attr(args)
|
602
|
+
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file])
|
603
|
+
self['_attachments'][args[:name]] = {
|
604
|
+
'content-type' => content_type,
|
605
|
+
'data' => encode_attachment(args[:file].read)
|
606
|
+
}
|
607
|
+
end
|
608
|
+
|
609
|
+
include ::Extlib::Hook
|
610
|
+
register_instance_hooks :save, :destroy
|
611
|
+
|
612
|
+
end # class Model
|
613
|
+
end # module CouchRest
|