akdubya-cushion 0.5.2 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +94 -174
- data/lib/cushion.rb +54 -10
- data/lib/cushion/database.rb +294 -203
- data/lib/cushion/design.rb +4 -5
- data/lib/cushion/document.rb +119 -69
- data/lib/cushion/server.rb +9 -13
- data/spec/cushion_spec.rb +0 -2
- data/spec/database_spec.rb +215 -132
- data/spec/document_spec.rb +172 -0
- data/spec/server_spec.rb +52 -66
- metadata +3 -2
data/README.rdoc
CHANGED
@@ -1,226 +1,146 @@
|
|
1
1
|
= Cushion: A slim CouchDB client
|
2
2
|
|
3
|
-
Cushion is a
|
4
|
-
|
5
|
-
|
3
|
+
Cushion is a Ruby client for accessing CouchDB servers that's light on the
|
4
|
+
extras and heavy on the syntatical sugar.
|
5
|
+
|
6
|
+
Credit goes to CouchRest (http://github.com/jchris/couchrest/tree/master) for
|
7
|
+
the inspiration.
|
6
8
|
|
7
9
|
== Getting Started
|
8
10
|
|
9
11
|
# myapp.rb
|
10
12
|
require 'cushion'
|
11
13
|
|
12
|
-
db = Cushion
|
13
|
-
db.save_doc("_id" => "abc", "foo" => "bar")
|
14
|
-
db.open_doc("abc")
|
15
|
-
|
16
|
-
== Servers
|
17
|
-
|
18
|
-
The server location defaults to http://127.0.0.1:5984, so you may omit the host
|
19
|
-
when initializing a server if you are satisfied with the default.
|
20
|
-
|
21
|
-
Cushion.server
|
22
|
-
Cushion.server('http://myhost:6969')
|
23
|
-
|
24
|
-
Display the server welcome message:
|
25
|
-
|
26
|
-
server.info
|
27
|
-
|
28
|
-
Get a list of all databases on the server:
|
14
|
+
db = Cushion!(mydb)
|
29
15
|
|
30
|
-
|
16
|
+
== Document Basics
|
31
17
|
|
32
|
-
|
18
|
+
Storing documents is simple. Just supply a hash of attributes. Leave out the
|
19
|
+
<tt>_id</tt> attribute if you want to use an auto-generated UUID. Set the
|
20
|
+
<tt>_rev</tt> attribute to update a document:
|
33
21
|
|
34
|
-
|
22
|
+
db.store("baz" => "bat")
|
23
|
+
# => {"ok"=>true, "id"=> "bdd39a3f9a1e5894d3d1283aa0d0f53f", "rev"=> "2699240268"}
|
35
24
|
|
36
|
-
|
25
|
+
response = db.store("_id" => "mydoc", "foo" => "bar")
|
26
|
+
# => {"ok"=>true, "id"=>"mydoc", "rev"=>"882534292"}
|
37
27
|
|
38
|
-
|
28
|
+
db.store("_id" => "mydoc", "foo" => "baz", "_rev" => response['rev'])
|
29
|
+
# => {"ok"=>true, "id"=>"mydoc", "rev"=>"3617508982"}
|
39
30
|
|
40
|
-
|
31
|
+
Fetching documents is just as simple.
|
41
32
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
server.restart
|
47
|
-
|
48
|
-
Obtain server stats:
|
49
|
-
|
50
|
-
server.stats
|
51
|
-
|
52
|
-
Replicate from a source database to a target database:
|
53
|
-
|
54
|
-
server.replicate("db1", "db2")
|
55
|
-
server.replicate("db1", "http://host2:5984/bar")
|
33
|
+
db.key?("mydoc") # => true
|
34
|
+
db.fetch("mydoc") # => {"_id"=>"mydoc", "_rev"=>"3617508982", "foo"=>"baz"}
|
35
|
+
db.fetch("bogusid") # => raises RestClient::ResourceNotFound
|
56
36
|
|
57
|
-
|
37
|
+
# Use etags to perform a conditional GET
|
38
|
+
db.fetch("mydoc", :etag => cur_rev) # => raises RestClient::NotModified
|
58
39
|
|
59
|
-
|
60
|
-
|
40
|
+
Store attachments inline or via CouchDB's standalone attachment API. The standalone
|
41
|
+
method accepts both IO objects and strings:
|
61
42
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
Create a database directly from a url, or just return the database instance if
|
69
|
-
it already exists:
|
70
|
-
|
71
|
-
Cushion.db!('http://127.0.0.1:5984/mydb')
|
72
|
-
|
73
|
-
Compact a database, eliminating old document revisions:
|
74
|
-
|
75
|
-
db.compact
|
76
|
-
|
77
|
-
Create and delete a database from an instance of <tt>Cushion::Database</tt>
|
78
|
-
|
79
|
-
db.create
|
80
|
-
db.delete
|
81
|
-
|
82
|
-
Open a document:
|
83
|
-
|
84
|
-
db.open_doc("mydoc")
|
43
|
+
# Inline
|
44
|
+
db.store("_id" => "hasfiles", ..., "_attachments" => {
|
45
|
+
"foo.txt" => {
|
46
|
+
"content_type" => "text/plain",
|
47
|
+
"data" => "Hello World!"
|
48
|
+
}})
|
85
49
|
|
86
|
-
|
50
|
+
# Standalone
|
51
|
+
res = @db.store("_id" => "savemefirst", ...)
|
52
|
+
db.attach("savemefirst", "foo.txt", "Hello World!", :rev => res['rev'],
|
53
|
+
:content_type => "text/plain")
|
54
|
+
# => Foo now contains an attachment hash similar to the above example
|
87
55
|
|
88
|
-
|
56
|
+
# Fetch attachment data
|
57
|
+
db.fetch("savemefirst", "foo.txt") # => "Hello World!"
|
89
58
|
|
90
|
-
|
59
|
+
Cushion returns parsed JSON by default. In those cases where you need the raw
|
60
|
+
JSON string, simply set an accept header:
|
91
61
|
|
92
|
-
db.
|
93
|
-
|
94
|
-
ab.save_doc("_id" => "exists", "_rev" => "1234", .. attributes ..)
|
62
|
+
db.fetch("mydoc", :headers => { :accept => "text/plain" })
|
63
|
+
# => "{\"_id\":\"mydoc\",\"_rev\":\"3617508982\",\"foo\":\"baz\"}\n"
|
95
64
|
|
96
|
-
|
65
|
+
The technique above works for nearly every <tt>Cushion</tt> request method.
|
97
66
|
|
98
|
-
|
67
|
+
== Document Macros
|
99
68
|
|
100
|
-
|
69
|
+
You can use CouchDB's bulk docs feature to create, update and delete many
|
70
|
+
documents at once:
|
101
71
|
|
102
72
|
docs = [
|
103
|
-
{ "_id" => "0", "_rev" => "123456", "_deleted" => true }, #=> Delete
|
104
|
-
{ "_id" => "1", "_rev" => "32486671", "foo" => "bar" }, #=> Update
|
105
|
-
{ "_id" => "2", "baz" => "bat" } #=> Create
|
73
|
+
{ "_id" => "0", "_rev" => "123456", "_deleted" => true }, #=> Delete
|
74
|
+
{ "_id" => "1", "_rev" => "32486671", "foo" => "bar" }, #=> Update
|
75
|
+
{ "_id" => "2", "baz" => "bat" } #=> Create
|
106
76
|
]
|
107
77
|
|
108
|
-
db.
|
78
|
+
db.bulk(docs) # => returns a hash of updated documents
|
79
|
+
db.bulk(docs, :delete => true) # => deletes the selected docs
|
109
80
|
|
110
|
-
|
81
|
+
Documents and attachments may also be copied or moved within the same database.
|
82
|
+
Some useful examples:
|
111
83
|
|
112
|
-
|
84
|
+
# Documents
|
85
|
+
db.copy("mydoc", "new_doc")
|
86
|
+
# => {"ok"=>true, "id"=>"new_doc", "rev"=> ...}
|
87
|
+
db.move("mydoc", "existing_doc", :rev => ..., :dest_rev => ...)
|
88
|
+
# => overwrites the existing doc
|
113
89
|
|
114
|
-
|
90
|
+
# Attachments
|
91
|
+
db.copy_attachment("mydoc", "foo.txt", "bar.txt")
|
92
|
+
# => copies an attachment within the same doc
|
93
|
+
db.move_attachment("mydoc", "foo.txt", "bar.txt")
|
94
|
+
# => renames an attachment; aliased at #rename_attachment
|
115
95
|
|
116
|
-
|
117
|
-
"1" => ["12345", "42836"],
|
118
|
-
"2" => ["572654"],
|
119
|
-
"3" => ["34462"]
|
120
|
-
}
|
121
|
-
|
122
|
-
db.purge(doc_revs)
|
123
|
-
|
124
|
-
Copy or move a document to a new +id+. Specify a +dest_rev+ to overwrite an existing
|
125
|
-
document:
|
126
|
-
|
127
|
-
db.copy_doc("doc1", "doc2")
|
128
|
-
db.copy_doc("doc1", "doc2", :dest_rev => "36452")
|
129
|
-
db.move_doc("doc1", "1234", "doc2")
|
130
|
-
db.move_doc("doc1", "1234", "doc2", :dest_rev => "37462")
|
131
|
-
|
132
|
-
CouchDB can attach multiple files directly to documents, either as inline Base64
|
133
|
-
encoded data or via a standalone attachment API.
|
134
|
-
|
135
|
-
Open a document attachment. This will return an <tt>IO</tt> object:
|
136
|
-
|
137
|
-
temp = db.open_attachment("mydoc", "foo.txt")
|
138
|
-
temp.read
|
139
|
-
|
140
|
-
Save an inline attachment to a document:
|
141
|
-
|
142
|
-
db.save_doc("_id" => "mydoc", "_attachments" => {
|
143
|
-
"foo.txt" => {
|
144
|
-
"content_type" => "text/plain",
|
145
|
-
"data" => "Hello World!"
|
146
|
-
}
|
147
|
-
})
|
148
|
-
|
149
|
-
Save a standalone attachment to a saved document. Content type defaults to
|
150
|
-
"application/octet-stream":
|
151
|
-
|
152
|
-
db.save_attachment("mydoc", "1234", "bar.txt", "somedata", :content_type => "text/plain")
|
153
|
-
|
154
|
-
Delete a document attachment:
|
155
|
-
|
156
|
-
db.delete_attachment("mydoc", "1234", "foo.txt")
|
157
|
-
|
158
|
-
Rename an attachment:
|
159
|
-
|
160
|
-
db.rename_attachment("mydoc", "1234", "baz.txt", "bat.txt")
|
96
|
+
See the database specs for more copy and move examples.
|
161
97
|
|
162
98
|
== Views
|
163
99
|
|
164
|
-
|
100
|
+
- creating (design docs)
|
101
|
+
- querying
|
102
|
+
- temp views
|
165
103
|
|
166
|
-
|
167
|
-
db.temp_view(temp_view, :key => "verified")
|
104
|
+
== Servers and Databases
|
168
105
|
|
169
|
-
|
170
|
-
|
171
|
-
db.view("people/by_first_name", :key => "gomer")
|
172
|
-
|
173
|
-
Show and list functions:
|
174
|
-
|
175
|
-
db.show("examples", "people", "mydoc", :format => "xml")
|
176
|
-
db.list("examples", "browse-people", "people-by-name", :startkey => ["a"], :limit => 10)
|
106
|
+
The server location defaults to http://127.0.0.1:5984, so you may omit the host
|
107
|
+
when initializing a server if you are satisfied with the default.
|
177
108
|
|
178
|
-
|
109
|
+
NOTE: Some cruft needs cleaning up. Use <tt>Cushion!(dbname, opts)</tt> for
|
110
|
+
now.
|
179
111
|
|
180
|
-
|
112
|
+
Display various bits of server metadata as follows:
|
181
113
|
|
182
|
-
|
183
|
-
|
114
|
+
server.info #=> welcome message
|
115
|
+
server.all_dbs #=> all dbs available
|
116
|
+
server.active_tasks #=> running tasks (e.g., replication or compaction)
|
117
|
+
server.config #=> server config
|
118
|
+
server.restart #=> restart the server
|
119
|
+
server.stats #=> detailed runtime stats
|
184
120
|
|
185
|
-
|
121
|
+
<tt>Cushion::Server</tt> can also replicate local and remote databases:
|
186
122
|
|
187
|
-
|
123
|
+
server.replicate("db1", "db2")
|
124
|
+
server.replicate("db1", "http://host2:5984/bar")
|
188
125
|
|
189
|
-
|
190
|
-
cases it is useful to return the raw response from CouchDB. Simply set the correct
|
191
|
-
HTTP headers to request an alternate format:
|
126
|
+
Creating database instances:
|
192
127
|
|
193
|
-
|
128
|
+
- top level methods
|
129
|
+
- options
|
194
130
|
|
195
|
-
|
196
|
-
the explicit headers key like so:
|
131
|
+
DB Operations:
|
197
132
|
|
198
|
-
db.
|
133
|
+
db.create
|
134
|
+
db.delete
|
135
|
+
db.compact
|
199
136
|
|
200
137
|
== Cushion::Document
|
201
138
|
|
202
|
-
|
203
|
-
individual documents.
|
204
|
-
|
205
|
-
Create a document:
|
206
|
-
|
207
|
-
Cushion::Document.use_database(db)
|
208
|
-
doc = Cushion::Document.new("foo" => "bar")
|
209
|
-
doc.save
|
210
|
-
|
211
|
-
Open a document and return an instance of <tt>Cushion::Document</tt>:
|
212
|
-
|
213
|
-
db.doc("mydoc")
|
214
|
-
doc["_foo"] = "bar"
|
215
|
-
doc.save
|
216
|
-
|
217
|
-
Copy a document:
|
218
|
-
|
219
|
-
doc.copy("mydoc2")
|
139
|
+
== Tricks
|
220
140
|
|
221
|
-
|
141
|
+
Use the #headers method to obtain response headers on any request:
|
222
142
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
143
|
+
db.fetch("mydoc").headers
|
144
|
+
# => {:server=>"CouchDB/0.9.0a (Erlang OTP/R12B)", :etag=>"\"3617508982\"",
|
145
|
+
:date=>"Fri, 06 Mar 2009 10:21:59 GMT", :content_type=>"application/json",
|
146
|
+
:content_length=>"48", :cache_control=>"must-revalidate"}
|
data/lib/cushion.rb
CHANGED
@@ -12,14 +12,22 @@ require dir + 'design'
|
|
12
12
|
|
13
13
|
DEFAULT_COUCH_HOST = "http://127.0.0.1:5984"
|
14
14
|
|
15
|
+
def Cushion(dbname, opts = {})
|
16
|
+
Cushion.server(opts).db(dbname)
|
17
|
+
end
|
18
|
+
|
19
|
+
def Cushion!(dbname, opts = {})
|
20
|
+
Cushion.server(opts).db!(dbname)
|
21
|
+
end
|
22
|
+
|
15
23
|
module Cushion
|
16
|
-
VERSION = '0.
|
24
|
+
VERSION = '0.6.0'
|
17
25
|
|
18
26
|
class << self
|
19
27
|
|
20
28
|
# Returns a Cushion::Server instance.
|
21
|
-
def server(
|
22
|
-
Server.new(
|
29
|
+
def server(options = {})
|
30
|
+
Server.new(options)
|
23
31
|
end
|
24
32
|
alias_method :new, :server
|
25
33
|
|
@@ -27,7 +35,7 @@ module Cushion
|
|
27
35
|
# to create the database on the server.
|
28
36
|
def db(url)
|
29
37
|
parsed = parse(url)
|
30
|
-
server = Cushion.new(parsed[:host])
|
38
|
+
server = Cushion.new(:uri => parsed[:host])
|
31
39
|
server.db(parsed[:db])
|
32
40
|
end
|
33
41
|
alias_method :database, :db
|
@@ -36,7 +44,7 @@ module Cushion
|
|
36
44
|
# instance.
|
37
45
|
def db!(url)
|
38
46
|
parsed = parse(url)
|
39
|
-
server = Cushion.new(parsed[:host])
|
47
|
+
server = Cushion.new(:uri => parsed[:host])
|
40
48
|
server.db!(parsed[:db])
|
41
49
|
end
|
42
50
|
alias_method :database!, :db!
|
@@ -45,7 +53,7 @@ module Cushion
|
|
45
53
|
# instance.
|
46
54
|
def recreate(url)
|
47
55
|
parsed = parse(url)
|
48
|
-
server = Cushion.new(parsed[:host])
|
56
|
+
server = Cushion.new(:uri => parsed[:host])
|
49
57
|
server.recreate(parsed[:db])
|
50
58
|
end
|
51
59
|
|
@@ -61,9 +69,13 @@ module Cushion
|
|
61
69
|
url
|
62
70
|
end
|
63
71
|
|
64
|
-
# Handles
|
65
|
-
def
|
66
|
-
|
72
|
+
# Handles key escaping.
|
73
|
+
def escape_key(id, filename = nil)
|
74
|
+
if filename
|
75
|
+
"#{escape_docid(id)}/#{CGI.escape(filename)}"
|
76
|
+
else
|
77
|
+
escape_docid(id)
|
78
|
+
end
|
67
79
|
end
|
68
80
|
|
69
81
|
# Base64 encodes inline attachments.
|
@@ -78,7 +90,7 @@ module Cushion
|
|
78
90
|
def base64(data)
|
79
91
|
Base64.encode64(data).gsub(/\s/,'')
|
80
92
|
end
|
81
|
-
|
93
|
+
|
82
94
|
# Sets the RestClient proxy.
|
83
95
|
def proxy(url)
|
84
96
|
RestClient.proxy = url
|
@@ -86,6 +98,10 @@ module Cushion
|
|
86
98
|
|
87
99
|
private
|
88
100
|
|
101
|
+
def escape_docid(id)
|
102
|
+
/^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
|
103
|
+
end
|
104
|
+
|
89
105
|
def parse(url)
|
90
106
|
parsed = URI.parse(url)
|
91
107
|
{
|
@@ -94,4 +110,32 @@ module Cushion
|
|
94
110
|
}
|
95
111
|
end
|
96
112
|
end # class << self
|
113
|
+
|
114
|
+
# Provides a clean method of adding metadata (e.g., headers) to JSON parsed
|
115
|
+
# responses. Inspired by <tt>OpenURI::Meta</tt>.
|
116
|
+
module Meta
|
117
|
+
def Meta.init(obj, src=nil)
|
118
|
+
obj.extend Meta
|
119
|
+
obj.instance_eval {
|
120
|
+
@headers = {}
|
121
|
+
}
|
122
|
+
if src
|
123
|
+
src.headers.each {|name, value|
|
124
|
+
obj.add_header_field(name, value)
|
125
|
+
}
|
126
|
+
end
|
127
|
+
obj
|
128
|
+
end
|
129
|
+
|
130
|
+
attr_accessor :headers
|
131
|
+
|
132
|
+
def add_header_field(name, value)
|
133
|
+
#value.gsub!('"', '') if name == :etag ==> CouchDB seems to require the xtra "'s
|
134
|
+
@headers[name] = value
|
135
|
+
end
|
136
|
+
|
137
|
+
def etag
|
138
|
+
@headers[:etag]
|
139
|
+
end
|
140
|
+
end
|
97
141
|
end
|
data/lib/cushion/database.rb
CHANGED
@@ -4,92 +4,157 @@ require 'base64'
|
|
4
4
|
module Cushion
|
5
5
|
class Database
|
6
6
|
attr_reader :server, :name
|
7
|
+
attr_accessor :use_tempfiles
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
# Initializes a Cushion::Database instance.
|
10
|
+
def initialize(name, options = {})
|
11
|
+
unless options[:server]
|
12
|
+
raise ArgumentError, "server option must be provided"
|
13
|
+
end
|
14
|
+
unless name
|
15
|
+
raise ArgumentError, "name must be provided"
|
16
|
+
end
|
10
17
|
# TODO: CouchDB has strict db naming requirements. Should validate.
|
11
18
|
@name = CGI.escape(name.to_s)
|
12
|
-
@server = server
|
13
|
-
|
14
|
-
|
15
|
-
# Retrieves information about this database.
|
16
|
-
def info(headers = {})
|
17
|
-
server.get(@name, headers)
|
18
|
-
end
|
19
|
-
|
20
|
-
# Compacts this database.
|
21
|
-
def compact(headers = {})
|
22
|
-
post("_compact", nil, headers)
|
23
|
-
end
|
24
|
-
|
25
|
-
# Creates this database on the server.
|
26
|
-
def create(headers = {})
|
27
|
-
server.put(@name, nil, headers)
|
19
|
+
@server = options[:server]
|
20
|
+
@use_tempfiles = options[:use_tempfiles]
|
28
21
|
end
|
29
22
|
|
30
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# Query the default +all_docs+ view. Set the +keys+ option to perform a
|
36
|
-
# key-based multi-document fetch. Set the +headers+ option to pass
|
37
|
-
# custom request headers.
|
23
|
+
# Retrieves a single document or attachment by key. Returns attachments as
|
24
|
+
# an <tt>OpenURI</tt> <tt>IO</tt> object if the +use_tempfiles+ database
|
25
|
+
# option has been set. Set the +headers+ option to pass custom request headers.
|
26
|
+
# Examples:
|
38
27
|
#
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
28
|
+
# db.fetch("mydoc")
|
29
|
+
# db.fetch("my/doc") # Forward slashes are automatically escaped...
|
30
|
+
# db.fetch("_design/foo") # ...except in the case of design docs
|
31
|
+
#
|
32
|
+
# Attachments can be fetched by supplying the id and filename as follows:
|
33
|
+
#
|
34
|
+
# db.fetch("mydoc", "foo.txt")
|
35
|
+
# db.fetch("mydoc", "path/to/foo.txt") # Virtual file path
|
36
|
+
#
|
37
|
+
# Cushion requests "application/json" by default, and response bodies will
|
38
|
+
# automatically be parsed as such. To request data from CouchDB in another
|
39
|
+
# (unparsed) format, simply set the +accept+ header:
|
40
|
+
#
|
41
|
+
# db.fetch("mydoc", :headers => { :accept => "text/plain" })
|
42
|
+
#
|
43
|
+
# Set <code>:head => true</code> to return the headers rather than the
|
44
|
+
# content body. Set <code>:etag => some_value</code> to perform a
|
45
|
+
# simple if-none-match conditional GET.
|
46
|
+
#
|
47
|
+
def fetch(*args)
|
48
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
49
|
+
id, file = args
|
50
|
+
slug = Cushion.escape_key(id, file)
|
51
|
+
headers = options.delete(:headers) || {}
|
52
|
+
if etag = options.delete(:etag)
|
53
|
+
headers.merge!(:if_none_match => "\"#{etag}\"")
|
47
54
|
end
|
55
|
+
head = options.delete(:head)
|
56
|
+
path = Cushion.paramify_url("#{slug}", options)
|
57
|
+
if head
|
58
|
+
return head(path, headers)
|
59
|
+
elsif file && @use_tempfiles
|
60
|
+
return server.open_attachment("#{@name}/#{path}")
|
61
|
+
end
|
62
|
+
get(path, headers)
|
48
63
|
end
|
49
64
|
|
50
|
-
#
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
65
|
+
# Returns true if a document key exists in this database. See #fetch.
|
66
|
+
def key?(*args)
|
67
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
68
|
+
id, file = args
|
69
|
+
!!fetch(id, file, options.merge(:head => true))
|
70
|
+
rescue RestClient::ResourceNotFound
|
71
|
+
false
|
57
72
|
end
|
73
|
+
alias_method :has_key?, :key?
|
58
74
|
|
59
75
|
# Retrieves a single document by +id+ and returns a <tt>Cushion::Document</tt>
|
60
76
|
# linked to this database.
|
61
77
|
#
|
62
|
-
def doc(id,
|
63
|
-
result =
|
64
|
-
|
78
|
+
def doc(id, options = {})
|
79
|
+
result = fetch(id.to_s, options)
|
80
|
+
|
81
|
+
ndoc = if /^_design/ =~ result["_id"]
|
82
|
+
Design.new(result)
|
83
|
+
else
|
84
|
+
Document.new(result)
|
85
|
+
end
|
86
|
+
|
65
87
|
ndoc.database = self
|
66
88
|
ndoc
|
67
89
|
end
|
68
90
|
alias_method :document, :doc
|
69
91
|
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
92
|
+
# Stores a document to the database. Pass a hash with the desired attributes.
|
93
|
+
# If an +_id+ attribute is not provided one will be generated automatically
|
94
|
+
# from the server UUID cache. Examples:
|
95
|
+
#
|
96
|
+
# db.store("foo" => "bar") #=> Creates a doc with an auto-generated key
|
97
|
+
# db.store("_id" => "def") #=> Creates a doc with key "def"
|
98
|
+
#
|
99
|
+
# To update a document, set the +_rev+ attribute on the document hash:
|
74
100
|
#
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
101
|
+
# db.store("_id" => "ghi", "_rev" => "1234")
|
102
|
+
#
|
103
|
+
# Inline attachments are automatically encoded within the document.
|
104
|
+
#
|
105
|
+
def store(doc, options = {})
|
106
|
+
headers = options.delete(:headers) || {}
|
107
|
+
if doc['_attachments']
|
108
|
+
doc['_attachments'] = Cushion.encode_attachments(doc['_attachments'])
|
79
109
|
end
|
80
|
-
if
|
81
|
-
slug = Cushion.
|
82
|
-
put("#{slug}",
|
110
|
+
res = if doc['_id']
|
111
|
+
slug = Cushion.escape_key(doc['_id'])
|
112
|
+
put("#{slug}", doc, headers)
|
83
113
|
else
|
84
|
-
slug =
|
85
|
-
put("#{slug}",
|
114
|
+
slug = doc['_id'] = server.next_uuid
|
115
|
+
put("#{slug}", doc, headers)
|
86
116
|
end
|
117
|
+
if res['ok']
|
118
|
+
doc['_id'] = res['id']
|
119
|
+
doc['_rev'] = res['rev']
|
120
|
+
end
|
121
|
+
res
|
87
122
|
end
|
88
123
|
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
124
|
+
# Save a file attachment under an existing document, or create a new document
|
125
|
+
# containing an attachment. +id+ is the document id. +filename+ is the name
|
126
|
+
# of the attachment (extensions may be omitted). Set the +rev+ option to store
|
127
|
+
# the attachment under an existing document. Example:
|
128
|
+
#
|
129
|
+
# db.attach("docid", "foo.txt", somedata, :rev => "1234")
|
130
|
+
# #=> Stores an attachment under docid
|
131
|
+
#
|
132
|
+
# db.attach("anotherid", "bar.jpg", :content_type => "image/jpeg")
|
133
|
+
# #=> Creates a new document containing the attachment. Also sets the
|
134
|
+
# content type.
|
135
|
+
#
|
136
|
+
# +data+ may be a <tt>String</tt>, or any object that responds to read.
|
137
|
+
#
|
138
|
+
# Content type defaults to 'application/octet-stream'.
|
139
|
+
#
|
140
|
+
def attach(id, filename, data, options = {})
|
141
|
+
headers = options.delete(:headers) || {}
|
142
|
+
headers[:content_type] = options.delete(:content_type) || "application/octet-stream"
|
143
|
+
rev = options.delete(:rev)
|
144
|
+
slug = Cushion.escape_key(id, filename)
|
145
|
+
rev_slug = "?rev=#{rev}" if rev
|
146
|
+
data = data.respond_to?(:read) ? data.read : data
|
147
|
+
put("#{slug}#{rev_slug}", data, headers)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Deletes a single document or attachment by key. The +rev+ option must be
|
151
|
+
# provided.
|
152
|
+
def destroy(*args)
|
153
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
154
|
+
headers = options.delete(:headers) || {}
|
155
|
+
id, file = args
|
156
|
+
slug = Cushion.escape_key(id, file)
|
157
|
+
delete("#{slug}?rev=#{options[:rev]}", headers)
|
93
158
|
end
|
94
159
|
|
95
160
|
# Creates, updates and deletes multiple documents in this database according
|
@@ -102,10 +167,10 @@ module Cushion
|
|
102
167
|
# { "_id" => "1", "_rev" => "32486671", "foo" => "bar" }, #=> Update this doc
|
103
168
|
# { "_id" => "2", "baz" => "bat" } #=> Create this doc
|
104
169
|
# ]
|
105
|
-
#
|
106
|
-
# db.bulk_docs(docs)
|
107
170
|
#
|
108
|
-
#
|
171
|
+
# db.bulk(docs)
|
172
|
+
#
|
173
|
+
# <tt>bulk</tt> returns a hash of updated doc ids and revs, as follows:
|
109
174
|
#
|
110
175
|
# {
|
111
176
|
# "ok" => true,
|
@@ -117,12 +182,19 @@ module Cushion
|
|
117
182
|
# }
|
118
183
|
#
|
119
184
|
# Set the +use_uuids+ option to generate UUIDs from the server cache before
|
120
|
-
#
|
185
|
+
# saving. Set the +headers+ option to pass custom request headers.
|
121
186
|
#
|
122
|
-
def
|
123
|
-
|
124
|
-
|
125
|
-
|
187
|
+
def bulk(docs, options = {})
|
188
|
+
delete_all = options.delete(:delete)
|
189
|
+
headers = options.delete(:headers) || {}
|
190
|
+
use_uuids = options.delete(:use_uuids) || true
|
191
|
+
if delete_all
|
192
|
+
updates = docs.map { |doc|
|
193
|
+
{ '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true } }
|
194
|
+
else
|
195
|
+
updates = docs
|
196
|
+
end
|
197
|
+
if (use_uuids)
|
126
198
|
ids, noids = docs.partition{|d|d['_id']}
|
127
199
|
uuid_count = [noids.length, server.uuid_batch_count].max
|
128
200
|
noids.each do |doc|
|
@@ -130,114 +202,144 @@ module Cushion
|
|
130
202
|
doc['_id'] = nextid if nextid
|
131
203
|
end
|
132
204
|
end
|
133
|
-
post("_bulk_docs", {:docs =>
|
134
|
-
end
|
135
|
-
|
136
|
-
# A convenience method for deleting multiple documents at once. Just pass in
|
137
|
-
# an array of saved +docs+ and the correct deletion params are
|
138
|
-
# automatically set. (See #bulk_docs)
|
139
|
-
#
|
140
|
-
def bulk_delete(docs, params = {})
|
141
|
-
deletions = docs.map { |doc| { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true } }
|
142
|
-
bulk_docs(deletions, params.merge(:use_uuids => false))
|
205
|
+
post("_bulk_docs", {:docs => updates}, headers)
|
143
206
|
end
|
144
207
|
|
145
|
-
#
|
146
|
-
# +
|
147
|
-
# to
|
148
|
-
#
|
149
|
-
# doc_revs = {
|
150
|
-
# "1" => ["12345", "42836"],
|
151
|
-
# "2" => ["572654"],
|
152
|
-
# "3" => ["34462"]
|
153
|
-
# }
|
208
|
+
# Copies the document at +source+ to a new or existing location at +destination+.
|
209
|
+
# Set the +dest_rev+ option to overwrite an existing document. Set the
|
210
|
+
# +headers+ option to pass custom request headers. Example:
|
154
211
|
#
|
155
|
-
#
|
212
|
+
# db.copy("doc1", "doc2")
|
213
|
+
# #=> Copies to a new location
|
214
|
+
# db.copy("doc1", "doc3", :dest_rev => "4567")
|
215
|
+
# #=> Overwrites an existing doc
|
156
216
|
#
|
157
|
-
def
|
158
|
-
|
217
|
+
def copy(source, destination, options = {})
|
218
|
+
headers = options.delete(:headers) || {}
|
219
|
+
dest_rev = options.delete(:dest_rev)
|
220
|
+
slug = Cushion.escape_key(source)
|
221
|
+
dest = if dest_rev
|
222
|
+
"#{destination}?rev=#{dest_rev}"
|
223
|
+
else
|
224
|
+
destination
|
225
|
+
end
|
226
|
+
server.copy("#{@name}/#{slug}", dest, headers)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Copies an attachment to a new location. This is presently experimental.
|
230
|
+
def copy_attachment(id, old_filename, new_filename, options = {})
|
231
|
+
headers = options.delete(:headers) || {}
|
232
|
+
src = fetch(id, old_filename)
|
233
|
+
headers[:content_type] = src.headers[:content_type]
|
234
|
+
dest_id = options[:dest_id] || id
|
235
|
+
opts = options[:dest_rev] ? { :rev => options[:dest_rev] } : {}
|
236
|
+
if opts.empty? && (id == dest_id)
|
237
|
+
opts = { :rev => src.headers[:etag] }
|
238
|
+
end
|
239
|
+
res = attach(dest_id, new_filename, src, { :headers => headers }.merge(opts))
|
240
|
+
if id == dest_id
|
241
|
+
res.merge("src_rev" => res["rev"])
|
242
|
+
else
|
243
|
+
res.merge("src_rev" => src.headers[:etag])
|
244
|
+
end
|
159
245
|
end
|
160
246
|
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
247
|
+
# Moves the document at +source+ to a new or existing location at +destination+.
|
248
|
+
# Set the +dest_rev+ option to overwrite an existing document. Set the
|
249
|
+
# +headers+ option to pass custom request headers. Example:
|
164
250
|
#
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
251
|
+
# db.move("doc1", "doc2", :rev => "1234")
|
252
|
+
# #=> Moves to a new location
|
253
|
+
# db.copy("doc1", "doc3", :rev => "1234", :dest_rev => "4567")
|
254
|
+
# #=> Overwrites an existing doc
|
255
|
+
#
|
256
|
+
def move(source, destination, options = {})
|
257
|
+
headers = options.delete(:headers) || {}
|
258
|
+
dest_rev = options.delete(:dest_rev)
|
259
|
+
slug = Cushion.escape_key(source)
|
260
|
+
path = Cushion.paramify_url("#{slug}", options)
|
261
|
+
dest = if dest_rev
|
262
|
+
"#{destination}?rev=#{dest_rev}"
|
171
263
|
else
|
172
|
-
|
264
|
+
destination
|
173
265
|
end
|
174
|
-
|
266
|
+
server.move("#{@name}/#{path}", dest, headers)
|
175
267
|
end
|
176
268
|
|
177
|
-
# Moves
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
def move_doc(source_id, dest_id, src_rev, params = {})
|
183
|
-
headers = params.delete(:headers) || {}
|
184
|
-
dest_rev = params[:dest_rev]
|
185
|
-
slug = Cushion.escape_docid(source_id)
|
186
|
-
destination = if dest_rev
|
187
|
-
"#{dest_id}?rev=#{dest_rev}"
|
269
|
+
# Moves an attachment to a new location. This is presently experimental.
|
270
|
+
def move_attachment(id, old_filename, new_filename, options = {})
|
271
|
+
res = copy_attachment(id, old_filename, new_filename, options)
|
272
|
+
if res["ok"]
|
273
|
+
destroy(id, old_filename, :rev => res["src_rev"])
|
188
274
|
else
|
189
|
-
|
275
|
+
res
|
190
276
|
end
|
191
|
-
move("#{slug}?rev=#{src_rev}", destination, headers)
|
192
277
|
end
|
278
|
+
alias_method :rename_attachment, :move_attachment
|
193
279
|
|
194
|
-
#
|
195
|
-
#
|
196
|
-
#
|
280
|
+
# Query a named view. Set +name+ to :all to query CouchDB's all_docs view.
|
281
|
+
# Set +name+ to :temp to query a temp_view. Set the +keys+ option to perform
|
282
|
+
# a key-based multi-document fetch against any view. Examples
|
197
283
|
#
|
198
284
|
# temp_view = { :map => "function(doc){emit(doc.status,null)}" }
|
199
|
-
# db.
|
285
|
+
# db.view(:temp, :funcs => temp_view, :key => "verified")
|
286
|
+
# db.view("people/by_first_name", :key => "gomer")
|
287
|
+
# db.view("foo/bar", :keys => ["a", "b", "c"])
|
288
|
+
# db.view(:all)
|
200
289
|
#
|
201
|
-
# Set the +headers+ option to pass custom request headers.
|
290
|
+
# Set the +headers+ option to pass custom request headers. Set
|
291
|
+
# <code>:etag => some_value</code> to perform a simple if-none-match
|
292
|
+
# conditional GET.
|
202
293
|
#
|
203
294
|
# Temp view queries are slow, so they should only be used as a convenience
|
204
|
-
# during development.
|
205
|
-
#
|
206
|
-
def temp_view(funcs, params = {})
|
207
|
-
keys = params.delete(:keys)
|
208
|
-
headers = params.delete(:headers) || {}
|
209
|
-
funcs = funcs.merge({:keys => keys}) if keys
|
210
|
-
path = Cushion.paramify_url("_temp_view", params)
|
211
|
-
post(path, funcs, headers)
|
212
|
-
end
|
213
|
-
|
214
|
-
# Query a saved view identified by +view+. Set the +keys+ param to perform a
|
215
|
-
# key-based multi-document fetch. Example:
|
295
|
+
# during development. Whenever possible you should query against saved
|
296
|
+
# views.
|
216
297
|
#
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
298
|
+
def view(name, options = {})
|
299
|
+
keys = options.delete(:keys)
|
300
|
+
headers = options.delete(:headers) || {}
|
301
|
+
if etag = options.delete(:etag)
|
302
|
+
headers.merge!(:if_none_match => "\"#{etag}\"".squeeze("\""))
|
303
|
+
end
|
304
|
+
body = keys ? {:keys => keys} : {}
|
305
|
+
if name == :all
|
306
|
+
slug = "_all_docs"
|
307
|
+
elsif name == :temp
|
308
|
+
slug = "_temp_view"
|
309
|
+
body.merge!(options.delete(:funcs))
|
310
|
+
else
|
311
|
+
slug = "_view/#{name}"
|
312
|
+
end
|
313
|
+
path = Cushion.paramify_url(slug, options)
|
314
|
+
|
315
|
+
if body.any?
|
316
|
+
post(path, body, headers)
|
227
317
|
else
|
228
318
|
get(path, headers)
|
229
319
|
end
|
230
320
|
end
|
231
321
|
|
322
|
+
# Convenience method for querying the all_docs view. See #view for more
|
323
|
+
# information.
|
324
|
+
def all(options = {})
|
325
|
+
view(:all, options)
|
326
|
+
end
|
327
|
+
|
328
|
+
# Convenience method for querying temporary views. See #view for more
|
329
|
+
# information.
|
330
|
+
def temp(funcs, options = {})
|
331
|
+
view(:temp, options.merge(:funcs => funcs))
|
332
|
+
end
|
333
|
+
|
232
334
|
# Query the show template identified by +design+, +show_template+ and +id+.
|
233
335
|
# The results are not parsed by default. Set the +headers+ option to pass
|
234
336
|
# custom request headers.
|
235
337
|
#
|
236
|
-
def show(design, show_template, id,
|
338
|
+
def show(design, show_template, id, options = {})
|
237
339
|
defaults = { :accept => "text/html;text/plain;*/*" }
|
238
|
-
headers =
|
239
|
-
slug = Cushion.
|
240
|
-
path = Cushion.paramify_url("_show/#{design}/#{show_template}/#{slug}",
|
340
|
+
headers = options.delete(:headers) || {}
|
341
|
+
slug = Cushion.escape_key(id)
|
342
|
+
path = Cushion.paramify_url("_show/#{design}/#{show_template}/#{slug}", options)
|
241
343
|
get(path, defaults.merge(headers))
|
242
344
|
end
|
243
345
|
|
@@ -245,65 +347,66 @@ module Cushion
|
|
245
347
|
# The results are not parsed by default. Set the +headers+ option to pass
|
246
348
|
# custom request headers.
|
247
349
|
#
|
248
|
-
def list(design, list_template, view,
|
350
|
+
def list(design, list_template, view, options = {})
|
249
351
|
defaults = { :accept => "text/html;text/plain;*/*" }
|
250
|
-
headers =
|
251
|
-
path = Cushion.paramify_url("_list/#{design}/#{list_template}/#{view}",
|
352
|
+
headers = options.delete(:headers) || {}
|
353
|
+
path = Cushion.paramify_url("_list/#{design}/#{list_template}/#{view}", options)
|
252
354
|
get(path, defaults.merge(headers))
|
253
355
|
end
|
254
356
|
|
255
|
-
#
|
256
|
-
|
257
|
-
|
258
|
-
#
|
259
|
-
def external(verb, process_path, params = {})
|
260
|
-
path = Cushion.paramify_url("_#{process_path}", params[:query])
|
261
|
-
request(verb, path, :body => params[:body], :headers => params[:headers])
|
357
|
+
# Retrieves information about this database.
|
358
|
+
def info(headers = {})
|
359
|
+
server.get(@name, headers)
|
262
360
|
end
|
263
361
|
|
264
|
-
#
|
265
|
-
|
266
|
-
|
267
|
-
def open_attachment(id, filename, params = {})
|
268
|
-
slug = Cushion.escape_docid(id)
|
269
|
-
fname = CGI.escape(filename)
|
270
|
-
path = Cushion.paramify_url("#{@name}/#{slug}/#{fname}", params)
|
271
|
-
server.open_attachment(path)
|
362
|
+
# Compacts this database.
|
363
|
+
def compact(headers = {})
|
364
|
+
post("_compact", nil, headers)
|
272
365
|
end
|
273
366
|
|
274
|
-
#
|
275
|
-
|
276
|
-
|
277
|
-
# as the last param.
|
278
|
-
#
|
279
|
-
def save_attachment(id, rev, filename, data, headers = {})
|
280
|
-
defaults = { :content_type => "application/octet-stream" }
|
281
|
-
slug = Cushion.escape_docid(id)
|
282
|
-
fname = CGI.escape(filename)
|
283
|
-
put("#{slug}/#{fname}?rev=#{rev}", data, defaults.merge(headers))
|
367
|
+
# Creates this database on the server.
|
368
|
+
def create(headers = {})
|
369
|
+
server.put(@name, nil, headers)
|
284
370
|
end
|
285
371
|
|
286
|
-
# Deletes
|
287
|
-
|
372
|
+
# Deletes this database from the server.
|
373
|
+
def drop(headers = {})
|
374
|
+
server.delete(@name, headers)
|
375
|
+
end
|
376
|
+
|
377
|
+
# Deletes and recreates this database.
|
378
|
+
def recreate
|
379
|
+
begin
|
380
|
+
drop
|
381
|
+
rescue RestClient::ResourceNotFound
|
382
|
+
nil
|
383
|
+
end
|
384
|
+
create
|
385
|
+
end
|
386
|
+
|
387
|
+
# Completely purges the supplied document revisions from the database.
|
388
|
+
# +doc_revs+ is a hash of document ids, each containing an array of revisions
|
389
|
+
# to be deleted. Example:
|
390
|
+
#
|
391
|
+
# doc_revs = {
|
392
|
+
# "1" => ["12345", "42836"],
|
393
|
+
# "2" => ["572654"],
|
394
|
+
# "3" => ["34462"]
|
395
|
+
# }
|
288
396
|
#
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
397
|
+
# db.purge(doc_revs)
|
398
|
+
#
|
399
|
+
def purge(doc_revs, options = {})
|
400
|
+
post("_purge", doc_revs, options)
|
293
401
|
end
|
294
402
|
|
295
|
-
#
|
296
|
-
#
|
297
|
-
#
|
403
|
+
# Issues a request to the CouchDB external server identified by +process+.
|
404
|
+
# Calls to external must include a request +verb+. Set the +headers+ option
|
405
|
+
# to pass custom request headers.
|
298
406
|
#
|
299
|
-
def
|
300
|
-
|
301
|
-
|
302
|
-
if res["ok"] == true
|
303
|
-
res = delete_attachment(id, res["rev"], oldname)
|
304
|
-
else
|
305
|
-
res
|
306
|
-
end
|
407
|
+
def external(verb, process_path, params = {})
|
408
|
+
path = Cushion.paramify_url("_#{process_path}", params[:query])
|
409
|
+
request(verb, path, :body => params[:body], :headers => params[:headers])
|
307
410
|
end
|
308
411
|
|
309
412
|
# Issues a HEAD request to this database. See Cushion::Server#head.
|
@@ -331,22 +434,10 @@ module Cushion
|
|
331
434
|
server.delete("#{@name}/#{path}", headers)
|
332
435
|
end
|
333
436
|
|
334
|
-
# Issues a COPY request to this database. See Cushion::Server#copy.
|
335
|
-
def copy(source, destination, headers = {})
|
336
|
-
server.copy("#{@name}/#{source}", destination, headers)
|
337
|
-
end
|
338
|
-
|
339
|
-
# Issues a MOVE request to this database. See Cushion::Server#move.
|
340
|
-
def move(source, destination, headers = {})
|
341
|
-
server.move("#{@name}/#{source}", destination, headers)
|
342
|
-
end
|
343
|
-
|
344
437
|
# Issues a generic request to this database. See Cushion::Server#request.
|
345
438
|
def request(verb, path, params)
|
346
439
|
server.request(verb, "#{@name}/#{path}", params)
|
347
440
|
end
|
348
441
|
|
349
|
-
## EXPERIMENTAL ##
|
350
|
-
|
351
442
|
end
|
352
443
|
end
|