namelessjon-couchrest 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +176 -0
- data/README.md +46 -0
- data/Rakefile +69 -0
- data/THANKS.md +21 -0
- data/couchrest.gemspec +111 -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/history.txt +145 -0
- data/lib/couchrest/commands/generate.rb +71 -0
- data/lib/couchrest/commands/push.rb +103 -0
- data/lib/couchrest/database.rb +373 -0
- data/lib/couchrest/design.rb +80 -0
- data/lib/couchrest/document.rb +89 -0
- data/lib/couchrest/helper/attachments.rb +29 -0
- data/lib/couchrest/helper/pager.rb +103 -0
- data/lib/couchrest/helper/streamer.rb +51 -0
- data/lib/couchrest/helper/upgrade.rb +52 -0
- data/lib/couchrest/json_response.rb +14 -0
- data/lib/couchrest/middlewares/logger.rb +263 -0
- data/lib/couchrest/monkeypatches.rb +42 -0
- data/lib/couchrest/response.rb +35 -0
- data/lib/couchrest/rest_api.rb +62 -0
- data/lib/couchrest/server.rb +90 -0
- data/lib/couchrest/support/inheritable_attributes.rb +107 -0
- data/lib/couchrest.rb +127 -0
- data/spec/couchrest/couchrest_spec.rb +202 -0
- data/spec/couchrest/database_spec.rb +870 -0
- data/spec/couchrest/design_spec.rb +158 -0
- data/spec/couchrest/document_spec.rb +279 -0
- data/spec/couchrest/helpers/pager_spec.rb +123 -0
- data/spec/couchrest/helpers/streamer_spec.rb +52 -0
- data/spec/couchrest/server_spec.rb +35 -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 +5 -0
- data/spec/spec_helper.rb +44 -0
- data/utils/remap.rb +27 -0
- data/utils/subset.rb +30 -0
- metadata +179 -0
data/history.txt
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
== Next Version
|
2
|
+
|
3
|
+
* Major enhancements
|
4
|
+
|
5
|
+
* Minor enhancements
|
6
|
+
* rest-client version mismatch between couchrest.rb & gemspec
|
7
|
+
|
8
|
+
== 1.0.0
|
9
|
+
|
10
|
+
* Major enhancements
|
11
|
+
* Moved ExtendedDocument and friends into own library, couchrest_extended_document. (Sam Lown)
|
12
|
+
* Removed HttpAbstraction component for direct interface with RestClient. (Sam Lown)
|
13
|
+
* Changed version to more conventional format starting from 1.0.0 to avoid ambiguity issues with order. (Sam Lown)
|
14
|
+
|
15
|
+
== 0.38
|
16
|
+
|
17
|
+
* Major enhancements
|
18
|
+
* Add create_target option to Database#replicate_to and #replicate_from. http://github.com/couchrest/couchrest/issues/#issue/26 (Alexander Uvarov)
|
19
|
+
* Removing unused core extensions and moving extlib_inhertiable_* methods to use couchrest_inheritable_*
|
20
|
+
to avoid conflicts with Rails. (Geoff Buesing)
|
21
|
+
|
22
|
+
* Minor enhancements
|
23
|
+
* Support for CouchDB 1.0
|
24
|
+
* Added Document#id= support (issue detected by Rory Franklin with RSpec model stubs)
|
25
|
+
* Fixing issues with CouchDB 1.0 and RestClient
|
26
|
+
|
27
|
+
== 0.37
|
28
|
+
|
29
|
+
* Minor enhancements
|
30
|
+
* Added gemspec (needed for Bundler install) (Tapajós)
|
31
|
+
|
32
|
+
== 0.36
|
33
|
+
|
34
|
+
* Major enhancements
|
35
|
+
* Adds support for continuous replication (sauy7)
|
36
|
+
* Automatic Type Casting (Alexander Uvarov, Sam Lown, Tim Heighes, Will Leinweber)
|
37
|
+
* Added a search method to CouchRest:Database to search the documents in a given database. (Dave Farkas, Arnaud Berthomier, John Wood)
|
38
|
+
|
39
|
+
* Minor enhancements
|
40
|
+
* Provide a description of the timeout error (John Wood)
|
41
|
+
|
42
|
+
== 0.35
|
43
|
+
|
44
|
+
* Major enhancements
|
45
|
+
* CouchRest::ExtendedDocument allow chaining the inherit class callback (Kenneth Kalmer) - http://github.com/couchrest/couchrest/issues#issue/8
|
46
|
+
|
47
|
+
* Minor enhancements
|
48
|
+
* Fix attachment bug (Johannes Jörg Schmidt)
|
49
|
+
* Fix create database exception bug (Damien Mathieu)
|
50
|
+
* Compatible with restclient >= 1.4.0 new responses (Julien Kirch)
|
51
|
+
* Bug fix: Attribute protection no longer strips attributes coming from the database (Will Leinweber)
|
52
|
+
* Bug fix: Remove double CGI escape when PUTting an attachment (nzoschke)
|
53
|
+
* Bug fix: Changing Class proxy to set database on result sets (Peter Gumeson)
|
54
|
+
* Bug fix: Updated time regexp (Nolan Darilek)
|
55
|
+
* Added an update_doc method to database to handle conflicts during atomic updates. (Pierre Larochelle)
|
56
|
+
* Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton)
|
57
|
+
|
58
|
+
== 0.34
|
59
|
+
|
60
|
+
* Major enhancements
|
61
|
+
|
62
|
+
* Added support for https database URIs. (Mathias Meyer)
|
63
|
+
* Changing some validations to be compatible with activemodel. (Marcos Tapajós)
|
64
|
+
* Adds attribute protection to properties. (Will Leinweber)
|
65
|
+
* Improved CouchRest::Database#save_doc, added "batch" mode to significantly speed up saves at cost of lower durability gurantees. (Igal Koshevoy)
|
66
|
+
* Added CouchRest::Database#bulk_save_doc and #batch_save_doc as human-friendlier wrappers around #save_doc. (Igal Koshevoy)
|
67
|
+
|
68
|
+
* Minor enhancements
|
69
|
+
|
70
|
+
* Fix content_type handling for attachments
|
71
|
+
* Fixed a bug in the pagination code that caused it to paginate over records outside of the scope of the view parameters.(John Wood)
|
72
|
+
* Removed amount_pages calculation for the pagination collection, since it cannot be reliably calculated without a view.(John Wood)
|
73
|
+
* Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton)
|
74
|
+
* Bug fix: http://github.com/couchrest/couchrest/issues/#issue/1 (Marcos Tapajós)
|
75
|
+
* Removed the Database class deprecation notices (Matt Aimonetti)
|
76
|
+
* Adding support to :cast_as => 'Date'. (Marcos Tapajós)
|
77
|
+
* Improve documentation (Marcos Tapajós)
|
78
|
+
* Streamer fixes (Julien Sanchez)
|
79
|
+
* Fix Save on Document & ExtendedDocument crashed if bulk (Julien Sanchez)
|
80
|
+
* Fix Initialization of ExtendentDocument model shouldn't failed on a nil value in argument (deepj)
|
81
|
+
* Change to use Jeweler and Gemcutter (Marcos Tapajós)
|
82
|
+
|
83
|
+
== 0.33
|
84
|
+
|
85
|
+
* Major enhancements
|
86
|
+
|
87
|
+
* Added a new Rack logger middleware letting you log/save requests/queries (Matt Aimonetti)
|
88
|
+
|
89
|
+
* Minor enhancements
|
90
|
+
|
91
|
+
* Added #amount_pages to a paginated result array (Matt Aimonetti)
|
92
|
+
* Ruby 1.9.2 compatible (Matt Aimonetti)
|
93
|
+
* Added a property? method for property cast as :boolean (John Wood)
|
94
|
+
* Added an option to force the deletion of a attachments (bypass 409s) (Matt Aimonetti)
|
95
|
+
* Created a new abstraction layer for the REST API (Matt Aimonetti)
|
96
|
+
* Bug fix: made ExtendedDocument#all compatible with Couch 0.10 (tc)
|
97
|
+
|
98
|
+
== 0.32
|
99
|
+
|
100
|
+
* Major enhancements
|
101
|
+
|
102
|
+
* ExtendedDocument.get doesn't raise an exception anymore. If no documents are found nil is returned.
|
103
|
+
* ExtendedDocument.get! works the say #get used to work and will raise an exception if a document isn't found.
|
104
|
+
|
105
|
+
* Minor enhancements
|
106
|
+
|
107
|
+
* Bug fix: Model.all(:keys => [1,2]) was not working (Matt Aimonetti)
|
108
|
+
* Added ValidationErrors#count in order to play nicely with Rails (Peter Wagenet)
|
109
|
+
* Bug fix: class proxy design doc refresh (Daniel Kirsh)
|
110
|
+
* Bug fix: the count method on the proxy collection was missing (Daniel Kirsch)
|
111
|
+
* Added #amount_pages to a paginated collection. (Matt Aimonetti)
|
112
|
+
|
113
|
+
== 0.31
|
114
|
+
|
115
|
+
* Major enhancements
|
116
|
+
|
117
|
+
* Created an abstraction HTTP layer to support different http adapters (Matt Aimonetti)
|
118
|
+
* Added ExtendedDocument.create({}) and #create!({}) so you don't have to do Model.new.create (Matt Aimonetti)
|
119
|
+
|
120
|
+
* Minor enhancements
|
121
|
+
|
122
|
+
* Added an init.rb file for easy usage as a Rails plugin (Aaron Quint)
|
123
|
+
* Bug fix: pagination shouldn't die on empty results (Arnaud Berthomier)
|
124
|
+
* Optimized ExtendedDocument.count to run about 3x faster (Matt Aimonetti)
|
125
|
+
* Added Float casting (Ryan Felton & Matt Aimonetti)
|
126
|
+
|
127
|
+
== 0.30
|
128
|
+
|
129
|
+
* Major enhancements
|
130
|
+
|
131
|
+
* Added support for pagination (John Wood)
|
132
|
+
* Improved performance when initializing documents with timestamps (Matt Aimonetti)
|
133
|
+
|
134
|
+
* Minor enhancements
|
135
|
+
|
136
|
+
* Extended the API to retrieve an attachment URI (Matt Aimonetti)
|
137
|
+
* Bug fix: default value should be able to be set as false (Alexander Uvarov)
|
138
|
+
* Bug fix: validates_is_numeric should be able to properly validate a Float instance (Rob Kaufman)
|
139
|
+
* Bug fix: fixed the Timeout implementation (Seth Falcon)
|
140
|
+
|
141
|
+
|
142
|
+
---
|
143
|
+
|
144
|
+
Unfortunately, before 0.30 we did not keep a track of the modifications made to CouchRest.
|
145
|
+
You can see the full commit history on GitHub: http://github.com/couchrest/couchrest/commits/master/
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module CouchRest
|
4
|
+
module Commands
|
5
|
+
module Generate
|
6
|
+
|
7
|
+
def self.run(options)
|
8
|
+
directory = options[:directory]
|
9
|
+
design_names = options[:trailing_args]
|
10
|
+
|
11
|
+
FileUtils.mkdir_p(directory)
|
12
|
+
filename = File.join(directory, "lib.js")
|
13
|
+
self.write(filename, <<-FUNC)
|
14
|
+
// Put global functions here.
|
15
|
+
// Include in your views with
|
16
|
+
//
|
17
|
+
// //include-lib
|
18
|
+
FUNC
|
19
|
+
|
20
|
+
design_names.each do |design_name|
|
21
|
+
subdirectory = File.join(directory, design_name)
|
22
|
+
FileUtils.mkdir_p(subdirectory)
|
23
|
+
filename = File.join(subdirectory, "sample-map.js")
|
24
|
+
self.write(filename, <<-FUNC)
|
25
|
+
function(doc) {
|
26
|
+
// Keys is first letter of _id
|
27
|
+
emit(doc._id[0], doc);
|
28
|
+
}
|
29
|
+
FUNC
|
30
|
+
|
31
|
+
filename = File.join(subdirectory, "sample-reduce.js")
|
32
|
+
self.write(filename, <<-FUNC)
|
33
|
+
function(keys, values) {
|
34
|
+
// Count the number of keys starting with this letter
|
35
|
+
return values.length;
|
36
|
+
}
|
37
|
+
FUNC
|
38
|
+
|
39
|
+
filename = File.join(subdirectory, "lib.js")
|
40
|
+
self.write(filename, <<-FUNC)
|
41
|
+
// Put functions specific to '#{design_name}' here.
|
42
|
+
// Include in your views with
|
43
|
+
//
|
44
|
+
// //include-lib
|
45
|
+
FUNC
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.help
|
50
|
+
helpstring = <<-GEN
|
51
|
+
|
52
|
+
Usage: couchview generate directory design1 design2 design3 ...
|
53
|
+
|
54
|
+
Couchview will create directories and example views for the design documents you specify.
|
55
|
+
|
56
|
+
GEN
|
57
|
+
helpstring.gsub(/^ /, '')
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.write(filename, contents)
|
61
|
+
puts "Writing #{filename}"
|
62
|
+
File.open(filename, "w") do |f|
|
63
|
+
# Remove leading spaces
|
64
|
+
contents.gsub!(/^ ( )?/, '')
|
65
|
+
f.write contents
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module CouchRest
|
2
|
+
|
3
|
+
module Commands
|
4
|
+
|
5
|
+
module Push
|
6
|
+
|
7
|
+
def self.run(options)
|
8
|
+
directory = options[:directory]
|
9
|
+
database = options[:trailing_args].first
|
10
|
+
|
11
|
+
fm = CouchRest::FileManager.new(database)
|
12
|
+
fm.loud = options[:loud]
|
13
|
+
|
14
|
+
if options[:loud]
|
15
|
+
puts "Pushing views from directory #{directory} to database #{fm.db}"
|
16
|
+
end
|
17
|
+
|
18
|
+
fm.push_views(directory)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.help
|
22
|
+
helpstring = <<-GEN
|
23
|
+
|
24
|
+
== Pushing views with Couchview ==
|
25
|
+
|
26
|
+
Usage: couchview push directory dbname
|
27
|
+
|
28
|
+
Couchview expects a specific filesystem layout for your CouchDB views (see
|
29
|
+
example below). It also supports advanced features like inlining of library
|
30
|
+
code (so you can keep DRY) as well as avoiding unnecessary document
|
31
|
+
modification.
|
32
|
+
|
33
|
+
Couchview also solves a problem with CouchDB's view API, which only provides
|
34
|
+
access to the final reduce side of any views which have both a map and a
|
35
|
+
reduce function defined. The intermediate map results are often useful for
|
36
|
+
development and production. CouchDB is smart enough to reuse map indexes for
|
37
|
+
functions duplicated across views within the same design document.
|
38
|
+
|
39
|
+
For views with a reduce function defined, Couchview creates both a reduce view
|
40
|
+
and a map-only view, so that you can browse and query the map side as well as
|
41
|
+
the reduction, with no performance penalty.
|
42
|
+
|
43
|
+
== Example ==
|
44
|
+
|
45
|
+
couchview push foo-project/bar-views baz-database
|
46
|
+
|
47
|
+
This will push the views defined in foo-project/bar-views into a database
|
48
|
+
called baz-database. Couchview expects the views to be defined in files with
|
49
|
+
names like:
|
50
|
+
|
51
|
+
foo-project/bar-views/my-design/viewname-map.js
|
52
|
+
foo-project/bar-views/my-design/viewname-reduce.js
|
53
|
+
foo-project/bar-views/my-design/noreduce-map.js
|
54
|
+
|
55
|
+
Pushed to => http://127.0.0.1:5984/baz-database/_design/my-design
|
56
|
+
|
57
|
+
And the design document:
|
58
|
+
{
|
59
|
+
"views" : {
|
60
|
+
"viewname-map" : {
|
61
|
+
"map" : "### contents of view-name-map.js ###"
|
62
|
+
},
|
63
|
+
"viewname-reduce" : {
|
64
|
+
"map" : "### contents of view-name-map.js ###",
|
65
|
+
"reduce" : "### contents of view-name-reduce.js ###"
|
66
|
+
},
|
67
|
+
"noreduce-map" : {
|
68
|
+
"map" : "### contents of noreduce-map.js ###"
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
Couchview will create a design document for each subdirectory of the views
|
74
|
+
directory specified on the command line.
|
75
|
+
|
76
|
+
== Library Inlining ==
|
77
|
+
|
78
|
+
Couchview can optionally inline library code into your views so you only have
|
79
|
+
to maintain it in one place. It looks for any files named lib.* in your
|
80
|
+
design-doc directory (for doc specific libs) and in the parent views directory
|
81
|
+
(for project global libs). These libraries are only inserted into views which
|
82
|
+
include the text
|
83
|
+
|
84
|
+
// !include lib
|
85
|
+
|
86
|
+
or
|
87
|
+
|
88
|
+
# !include lib
|
89
|
+
|
90
|
+
Couchview is a result of scratching my own itch. I'd be happy to make it more
|
91
|
+
general, so please contact me at jchris@grabb.it if you'd like to see anything
|
92
|
+
added or changed.
|
93
|
+
|
94
|
+
GEN
|
95
|
+
helpstring.gsub(/^ /, '')
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,373 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'couchrest/document'
|
3
|
+
require 'couchrest/design'
|
4
|
+
require 'couchrest/helper/streamer'
|
5
|
+
|
6
|
+
module CouchRest
|
7
|
+
class Database
|
8
|
+
attr_reader :server, :host, :name, :root, :uri
|
9
|
+
attr_accessor :bulk_save_cache_limit
|
10
|
+
|
11
|
+
# Create a CouchRest::Database adapter for the supplied CouchRest::Server
|
12
|
+
# and database name.
|
13
|
+
#
|
14
|
+
# ==== Parameters
|
15
|
+
# server<CouchRest::Server>:: database host
|
16
|
+
# name<String>:: database name
|
17
|
+
#
|
18
|
+
def initialize(server, name)
|
19
|
+
@name = name
|
20
|
+
@server = server
|
21
|
+
@host = server.uri
|
22
|
+
@uri = "/#{name.gsub('/','%2F')}"
|
23
|
+
@root = host + uri
|
24
|
+
@streamer = Streamer.new(self)
|
25
|
+
@bulk_save_cache = []
|
26
|
+
@bulk_save_cache_limit = 500 # must be smaller than the uuid count
|
27
|
+
end
|
28
|
+
|
29
|
+
# returns the database's uri
|
30
|
+
def to_s
|
31
|
+
@root
|
32
|
+
end
|
33
|
+
|
34
|
+
# GET the database info from CouchDB
|
35
|
+
def info
|
36
|
+
CouchRest.get @root
|
37
|
+
end
|
38
|
+
|
39
|
+
# Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
|
40
|
+
def documents(params = {})
|
41
|
+
keys = params.delete(:keys)
|
42
|
+
url = CouchRest.paramify_url "#{@root}/_all_docs", params
|
43
|
+
if keys
|
44
|
+
CouchRest.post(url, {:keys => keys})
|
45
|
+
else
|
46
|
+
CouchRest.get url
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Query a CouchDB-Lucene search view
|
51
|
+
def search(name, params={})
|
52
|
+
# -> http://localhost:5984/yourdb/_fti/YourDesign/by_name?include_docs=true&q=plop*'
|
53
|
+
url = CouchRest.paramify_url "#{root}/_fti/#{name}", params
|
54
|
+
CouchRest.get url
|
55
|
+
end
|
56
|
+
|
57
|
+
# load a set of documents by passing an array of ids
|
58
|
+
def get_bulk(ids)
|
59
|
+
documents(:keys => ids, :include_docs => true)
|
60
|
+
end
|
61
|
+
alias :bulk_load :get_bulk
|
62
|
+
|
63
|
+
# POST a temporary view function to CouchDB for querying. This is not
|
64
|
+
# recommended, as you don't get any performance benefit from CouchDB's
|
65
|
+
# materialized views. Can be quite slow on large databases.
|
66
|
+
def slow_view(funcs, params = {})
|
67
|
+
keys = params.delete(:keys)
|
68
|
+
funcs = funcs.merge({:keys => keys}) if keys
|
69
|
+
url = CouchRest.paramify_url "#{@root}/_temp_view", params
|
70
|
+
JSON.parse(RestClient.post(url, funcs.to_json, CouchRest.default_headers))
|
71
|
+
end
|
72
|
+
|
73
|
+
# backwards compatibility is a plus
|
74
|
+
alias :temp_view :slow_view
|
75
|
+
|
76
|
+
# Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
|
77
|
+
# paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
|
78
|
+
def view(name, params = {}, &block)
|
79
|
+
keys = params.delete(:keys)
|
80
|
+
name = name.split('/') # I think this will always be length == 2, but maybe not...
|
81
|
+
dname = name.shift
|
82
|
+
vname = name.join('/')
|
83
|
+
url = CouchRest.paramify_url "#{@root}/_design/#{dname}/_view/#{vname}", params
|
84
|
+
if keys
|
85
|
+
CouchRest.post(url, {:keys => keys})
|
86
|
+
else
|
87
|
+
if block_given?
|
88
|
+
@streamer.view("_design/#{dname}/_view/#{vname}", params, &block)
|
89
|
+
else
|
90
|
+
CouchRest.get url
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# GET a document from CouchDB, by id. Returns a Ruby Hash.
|
96
|
+
def get(id, params = {})
|
97
|
+
slug = escape_docid(id)
|
98
|
+
url = CouchRest.paramify_url("#{@root}/#{slug}", params)
|
99
|
+
result = CouchRest.get(url)
|
100
|
+
return result unless result.is_a?(Hash)
|
101
|
+
doc = if /^_design/ =~ result["_id"]
|
102
|
+
Design.new(result)
|
103
|
+
else
|
104
|
+
Document.new(result)
|
105
|
+
end
|
106
|
+
doc.database = self
|
107
|
+
doc
|
108
|
+
end
|
109
|
+
|
110
|
+
# GET an attachment directly from CouchDB
|
111
|
+
def fetch_attachment(doc, name)
|
112
|
+
uri = url_for_attachment(doc, name)
|
113
|
+
RestClient.get uri, CouchRest.default_headers
|
114
|
+
end
|
115
|
+
|
116
|
+
# PUT an attachment directly to CouchDB
|
117
|
+
def put_attachment(doc, name, file, options = {})
|
118
|
+
uri = url_for_attachment(doc, name)
|
119
|
+
JSON.parse(RestClient.put(uri, file, CouchRest.default_headers.merge(options)))
|
120
|
+
end
|
121
|
+
|
122
|
+
# DELETE an attachment directly from CouchDB
|
123
|
+
def delete_attachment(doc, name, force=false)
|
124
|
+
uri = url_for_attachment(doc, name)
|
125
|
+
# this needs a rev
|
126
|
+
begin
|
127
|
+
CouchRest.delete(uri)
|
128
|
+
rescue Exception => error
|
129
|
+
if force
|
130
|
+
# get over a 409
|
131
|
+
doc = get(doc['_id'])
|
132
|
+
uri = url_for_attachment(doc, name)
|
133
|
+
CouchRest.delete(uri)
|
134
|
+
else
|
135
|
+
error
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Save a document to CouchDB. This will use the <tt>_id</tt> field from
|
141
|
+
# the document as the id for PUT, or request a new UUID from CouchDB, if
|
142
|
+
# no <tt>_id</tt> is present on the document. IDs are attached to
|
143
|
+
# documents on the client side because POST has the curious property of
|
144
|
+
# being automatically retried by proxies in the event of network
|
145
|
+
# segmentation and lost responses.
|
146
|
+
#
|
147
|
+
# If <tt>bulk</tt> is true (false by default) the document is cached for bulk-saving later.
|
148
|
+
# Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
|
149
|
+
#
|
150
|
+
# If <tt>batch</tt> is true (false by default) the document is saved in
|
151
|
+
# batch mode, "used to achieve higher throughput at the cost of lower
|
152
|
+
# guarantees. When [...] sent using this option, it is not immediately
|
153
|
+
# written to disk. Instead it is stored in memory on a per-user basis for a
|
154
|
+
# second or so (or the number of docs in memory reaches a certain point).
|
155
|
+
# After the threshold has passed, the docs are committed to disk. Instead
|
156
|
+
# of waiting for the doc to be written to disk before responding, CouchDB
|
157
|
+
# sends an HTTP 202 Accepted response immediately. batch=ok is not suitable
|
158
|
+
# for crucial data, but it ideal for applications like logging which can
|
159
|
+
# accept the risk that a small proportion of updates could be lost due to a
|
160
|
+
# crash."
|
161
|
+
def save_doc(doc, bulk = false, batch = false)
|
162
|
+
if doc['_attachments']
|
163
|
+
doc['_attachments'] = encode_attachments(doc['_attachments'])
|
164
|
+
end
|
165
|
+
if bulk
|
166
|
+
@bulk_save_cache << doc
|
167
|
+
bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
|
168
|
+
return {"ok" => true} # Compatibility with Document#save
|
169
|
+
elsif !bulk && @bulk_save_cache.length > 0
|
170
|
+
bulk_save
|
171
|
+
end
|
172
|
+
result = if doc['_id']
|
173
|
+
slug = escape_docid(doc['_id'])
|
174
|
+
begin
|
175
|
+
uri = "#{@root}/#{slug}"
|
176
|
+
uri << "?batch=ok" if batch
|
177
|
+
CouchRest.put uri, doc
|
178
|
+
rescue RestClient::ResourceNotFound
|
179
|
+
p "resource not found when saving even tho an id was passed"
|
180
|
+
slug = doc['_id'] = @server.next_uuid
|
181
|
+
CouchRest.put "#{@root}/#{slug}", doc
|
182
|
+
end
|
183
|
+
else
|
184
|
+
begin
|
185
|
+
slug = doc['_id'] = @server.next_uuid
|
186
|
+
CouchRest.put "#{@root}/#{slug}", doc
|
187
|
+
rescue #old version of couchdb
|
188
|
+
CouchRest.post @root, doc
|
189
|
+
end
|
190
|
+
end
|
191
|
+
if result['ok']
|
192
|
+
doc['_id'] = result['id']
|
193
|
+
doc['_rev'] = result['rev']
|
194
|
+
doc.database = self if doc.respond_to?(:database=)
|
195
|
+
end
|
196
|
+
result
|
197
|
+
end
|
198
|
+
|
199
|
+
# Save a document to CouchDB in bulk mode. See #save_doc's +bulk+ argument.
|
200
|
+
def bulk_save_doc(doc)
|
201
|
+
save_doc(doc, true)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Save a document to CouchDB in batch mode. See #save_doc's +batch+ argument.
|
205
|
+
def batch_save_doc(doc)
|
206
|
+
save_doc(doc, false, true)
|
207
|
+
end
|
208
|
+
|
209
|
+
# POST an array of documents to CouchDB. If any of the documents are
|
210
|
+
# missing ids, supply one from the uuid cache.
|
211
|
+
#
|
212
|
+
# If called with no arguments, bulk saves the cache of documents to be bulk saved.
|
213
|
+
def bulk_save(docs = nil, use_uuids = true)
|
214
|
+
if docs.nil?
|
215
|
+
docs = @bulk_save_cache
|
216
|
+
@bulk_save_cache = []
|
217
|
+
end
|
218
|
+
if (use_uuids)
|
219
|
+
ids, noids = docs.partition{|d|d['_id']}
|
220
|
+
uuid_count = [noids.length, @server.uuid_batch_count].max
|
221
|
+
noids.each do |doc|
|
222
|
+
nextid = @server.next_uuid(uuid_count) rescue nil
|
223
|
+
doc['_id'] = nextid if nextid
|
224
|
+
end
|
225
|
+
end
|
226
|
+
CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
|
227
|
+
end
|
228
|
+
alias :bulk_delete :bulk_save
|
229
|
+
|
230
|
+
# DELETE the document from CouchDB that has the given <tt>_id</tt> and
|
231
|
+
# <tt>_rev</tt>.
|
232
|
+
#
|
233
|
+
# If <tt>bulk</tt> is true (false by default) the deletion is recorded for bulk-saving (bulk-deletion :) later.
|
234
|
+
# Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
|
235
|
+
def delete_doc(doc, bulk = false)
|
236
|
+
raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev']
|
237
|
+
if bulk
|
238
|
+
@bulk_save_cache << { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true }
|
239
|
+
return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
|
240
|
+
return { "ok" => true } # Mimic the non-deferred version
|
241
|
+
end
|
242
|
+
slug = escape_docid(doc['_id'])
|
243
|
+
CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
|
244
|
+
end
|
245
|
+
|
246
|
+
# COPY an existing document to a new id. If the destination id currently exists, a rev must be provided.
|
247
|
+
# <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
|
248
|
+
# hash with a '_rev' key
|
249
|
+
def copy_doc(doc, dest)
|
250
|
+
raise ArgumentError, "_id is required for copying" unless doc['_id']
|
251
|
+
slug = escape_docid(doc['_id'])
|
252
|
+
destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev']
|
253
|
+
"#{dest['_id']}?rev=#{dest['_rev']}"
|
254
|
+
else
|
255
|
+
dest
|
256
|
+
end
|
257
|
+
CouchRest.copy "#{@root}/#{slug}", destination
|
258
|
+
end
|
259
|
+
|
260
|
+
# Updates the given doc by yielding the current state of the doc
|
261
|
+
# and trying to update update_limit times. Returns the new doc
|
262
|
+
# if the doc was successfully updated without hitting the limit
|
263
|
+
def update_doc(doc_id, params = {}, update_limit=10)
|
264
|
+
resp = {'ok' => false}
|
265
|
+
new_doc = nil
|
266
|
+
last_fail = nil
|
267
|
+
|
268
|
+
until resp['ok'] or update_limit <= 0
|
269
|
+
doc = self.get(doc_id, params) # grab the doc
|
270
|
+
new_doc = yield doc # give it to the caller to be updated
|
271
|
+
begin
|
272
|
+
resp = self.save_doc new_doc # try to PUT the updated doc into the db
|
273
|
+
rescue RestClient::RequestFailed => e
|
274
|
+
if e.http_code == 409 # Update collision
|
275
|
+
update_limit -= 1
|
276
|
+
last_fail = e
|
277
|
+
else # some other error
|
278
|
+
raise e
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
raise last_fail unless resp['ok']
|
284
|
+
new_doc
|
285
|
+
end
|
286
|
+
|
287
|
+
# Compact the database, removing old document revisions and optimizing space use.
|
288
|
+
def compact!
|
289
|
+
CouchRest.post "#{@root}/_compact"
|
290
|
+
end
|
291
|
+
|
292
|
+
# Create the database
|
293
|
+
def create!
|
294
|
+
bool = server.create_db(@name) rescue false
|
295
|
+
bool && true
|
296
|
+
end
|
297
|
+
|
298
|
+
# Delete and re create the database
|
299
|
+
def recreate!
|
300
|
+
delete!
|
301
|
+
create!
|
302
|
+
rescue RestClient::ResourceNotFound
|
303
|
+
ensure
|
304
|
+
create!
|
305
|
+
end
|
306
|
+
|
307
|
+
# Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts.
|
308
|
+
def replicate_from(other_db, continuous = false, create_target = false)
|
309
|
+
replicate(other_db, continuous, :target => name, :create_target => create_target)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Replicates via "pushing" to another database. Makes no attempt to deal with conflicts.
|
313
|
+
def replicate_to(other_db, continuous = false, create_target = false)
|
314
|
+
replicate(other_db, continuous, :source => name, :create_target => create_target)
|
315
|
+
end
|
316
|
+
|
317
|
+
# DELETE the database itself. This is not undoable and could be rather
|
318
|
+
# catastrophic. Use with care!
|
319
|
+
def delete!
|
320
|
+
CouchRest.delete @root
|
321
|
+
end
|
322
|
+
|
323
|
+
private
|
324
|
+
|
325
|
+
def replicate(other_db, continuous, options)
|
326
|
+
raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
|
327
|
+
raise ArgumentError, "must provide a target or source option" unless (options.key?(:target) || options.key?(:source))
|
328
|
+
payload = options
|
329
|
+
if options.has_key?(:target)
|
330
|
+
payload[:source] = other_db.root
|
331
|
+
else
|
332
|
+
payload[:target] = other_db.root
|
333
|
+
end
|
334
|
+
payload[:continuous] = continuous
|
335
|
+
CouchRest.post "#{@host}/_replicate", payload
|
336
|
+
end
|
337
|
+
|
338
|
+
def uri_for_attachment(doc, name)
|
339
|
+
if doc.is_a?(String)
|
340
|
+
puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id"
|
341
|
+
docid = doc
|
342
|
+
rev = nil
|
343
|
+
else
|
344
|
+
docid = doc['_id']
|
345
|
+
rev = doc['_rev']
|
346
|
+
end
|
347
|
+
docid = escape_docid(docid)
|
348
|
+
name = CGI.escape(name)
|
349
|
+
rev = "?rev=#{doc['_rev']}" if rev
|
350
|
+
"/#{docid}/#{name}#{rev}"
|
351
|
+
end
|
352
|
+
|
353
|
+
def url_for_attachment(doc, name)
|
354
|
+
@root + uri_for_attachment(doc, name)
|
355
|
+
end
|
356
|
+
|
357
|
+
def escape_docid id
|
358
|
+
/^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
|
359
|
+
end
|
360
|
+
|
361
|
+
def encode_attachments(attachments)
|
362
|
+
attachments.each do |k,v|
|
363
|
+
next if v['stub']
|
364
|
+
v['data'] = base64(v['data'])
|
365
|
+
end
|
366
|
+
attachments
|
367
|
+
end
|
368
|
+
|
369
|
+
def base64(data)
|
370
|
+
[data].pack('m')
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|