throne 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -1
- data/{README.md → README.rdoc} +25 -8
- data/TODO +21 -0
- data/VERSION +1 -1
- data/lib/throne/database.rb +28 -7
- data/lib/throne/tasks.rb +233 -30
- metadata +6 -5
- data/FEATURES +0 -5
data/.gitignore
CHANGED
data/{README.md → README.rdoc}
RENAMED
@@ -1,10 +1,20 @@
|
|
1
|
-
|
1
|
+
= ♚ Throne
|
2
2
|
|
3
3
|
The king is here - on his couch, covered in rubies.
|
4
4
|
|
5
5
|
Simple library for working with CouchDB
|
6
6
|
|
7
|
-
|
7
|
+
== Caution!
|
8
|
+
|
9
|
+
This code is in _heavy_ development, in conjunction with a few development projects.
|
10
|
+
This means the API can and probably will change substantially over the next few
|
11
|
+
releases as we work out what fits best. You have been warned.
|
12
|
+
|
13
|
+
== API documentation
|
14
|
+
|
15
|
+
can be found at http://rdoc.info/projects/lstoll/throne
|
16
|
+
|
17
|
+
== Basic Usage
|
8
18
|
|
9
19
|
Create a database object to work with. Will create the DB if it doesn't exist
|
10
20
|
|
@@ -33,21 +43,28 @@ Delete a document
|
|
33
43
|
|
34
44
|
@db.delete(document_id_or_document_object)
|
35
45
|
|
36
|
-
Run a design document
|
46
|
+
Run a design document/function
|
37
47
|
|
38
|
-
@db.
|
48
|
+
res = @db.function('_design/DesignDoc/_view/viewname')
|
49
|
+
res
|
39
50
|
=> An array of documents
|
51
|
+
res.offset
|
52
|
+
=> couchdb offset data
|
40
53
|
|
41
54
|
# with parameters
|
42
|
-
@db.
|
55
|
+
@db.function('_design/DD/_list/listname/viewname', :key => ab..fg, :xyz => 7)
|
43
56
|
=> An array of documents
|
44
57
|
|
45
58
|
# Iterator Method
|
46
|
-
@db.
|
59
|
+
@db.function('_design/DesignDoc/_view/viewname') do |doc|
|
47
60
|
#invoked for each document
|
48
61
|
p doc
|
49
62
|
end
|
50
63
|
|
64
|
+
# All documents
|
65
|
+
@db.function('_all_docs')
|
66
|
+
=> All docs in the database
|
67
|
+
|
51
68
|
Delete the database
|
52
69
|
|
53
70
|
@db.delete_database
|
@@ -56,7 +73,7 @@ Create the database
|
|
56
73
|
|
57
74
|
@db.create_database
|
58
75
|
|
59
|
-
|
76
|
+
== Note on Patches/Pull Requests
|
60
77
|
|
61
78
|
* Fork the project.
|
62
79
|
* Make your feature addition or bug fix.
|
@@ -66,6 +83,6 @@ Create the database
|
|
66
83
|
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
67
84
|
* Send me a pull request. Bonus points for topic branches.
|
68
85
|
|
69
|
-
|
86
|
+
== Copyright
|
70
87
|
|
71
88
|
Copyright (c) 2009 Lincoln Stoll, Ben Schwarz. See LICENSE for details.
|
data/TODO
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
* Update the get method to take an existing doc, and reload it. Use ETags
|
2
|
+
* At the top level retrieved, look for a RubyType field, and cast
|
3
|
+
* vague AR methods (Create, save, hooks)
|
4
|
+
|
5
|
+
* Model Thoughts:
|
6
|
+
* Everything extends Dash.
|
7
|
+
* When a new Document is instantiated, it's RubyClass property is set to its class
|
8
|
+
* It's converted to JSON just by calling to_json on the parent 'document' class
|
9
|
+
* When it's parsed in, the overridden/delegated get method checks the type of the
|
10
|
+
parent object, and in instantiates it, passing the JSON hash in as a constuctor.
|
11
|
+
* The super constructor then loops the sub items, instantiating when it see's a
|
12
|
+
RubyClass object, otherwise just setting the value.
|
13
|
+
* There should be a different parent class for Master documents and the sub items.
|
14
|
+
sub item's don't have save/get methods - only useful when loading from a view.
|
15
|
+
|
16
|
+
* Bulk fetch, and maybe bulk update.
|
17
|
+
* Bulk fetch, get method checks for array param. If it's an array, POST with keys
|
18
|
+
* bulk put, save method checks for array param, and bundles into docs array
|
19
|
+
|
20
|
+
* Attachments
|
21
|
+
* Just a put to url/attchment with data attached. Data in as a #read object?
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.2
|
data/lib/throne/database.rb
CHANGED
@@ -33,9 +33,17 @@ class Throne::Database
|
|
33
33
|
end
|
34
34
|
|
35
35
|
# gets a document by it's ID
|
36
|
+
#
|
37
|
+
# @param [String] docid the ID of the document to retrieve
|
38
|
+
# @param [String] rev (optional) the revision of the document to retrieve
|
39
|
+
# @return [Hash, nil] the document mapped to a hash, or nil if not found.
|
36
40
|
def get(docid, rev=nil)
|
37
|
-
|
38
|
-
|
41
|
+
begin
|
42
|
+
revurl = rev ? "?rev=#{rev}" : ""
|
43
|
+
JSON.parse(c.get(@url + '/' + docid + revurl))
|
44
|
+
rescue RestClient::ResourceNotFound
|
45
|
+
nil
|
46
|
+
end
|
39
47
|
end
|
40
48
|
|
41
49
|
# creates/updates a document from a hash/array structure
|
@@ -62,10 +70,11 @@ class Throne::Database
|
|
62
70
|
c.delete(@url + '/' + doc + '?rev=' + rev)
|
63
71
|
end
|
64
72
|
|
65
|
-
# runs a
|
66
|
-
def
|
67
|
-
url = @url + '/
|
73
|
+
# runs a function by path, with optional params passed in
|
74
|
+
def function(path, params = {}, &block)
|
75
|
+
url = @url + '/' + path
|
68
76
|
res = JSON.parse(c.get(paramify_url(url, params)))
|
77
|
+
res = Throne::ArrayWithFunctionMeta.new(res['rows'], res['offset'])
|
69
78
|
if block_given?
|
70
79
|
# TODO - stream properly
|
71
80
|
res.each do |i|
|
@@ -77,8 +86,6 @@ class Throne::Database
|
|
77
86
|
end
|
78
87
|
end
|
79
88
|
|
80
|
-
|
81
|
-
|
82
89
|
private
|
83
90
|
|
84
91
|
def paramify_url url, params = {}
|
@@ -96,7 +103,9 @@ class Throne::Database
|
|
96
103
|
def c; RestClient; end
|
97
104
|
end
|
98
105
|
|
106
|
+
# Extended string, to store the couch revision
|
99
107
|
class Throne::StringWithRevision < String
|
108
|
+
# Couch revision ID
|
100
109
|
attr_reader :revision
|
101
110
|
|
102
111
|
def initialize(id, rev)
|
@@ -104,3 +113,15 @@ class Throne::StringWithRevision < String
|
|
104
113
|
super(id)
|
105
114
|
end
|
106
115
|
end
|
116
|
+
|
117
|
+
# Extended array, to store the couch extra data
|
118
|
+
class Throne::ArrayWithFunctionMeta < DelegateClass(Array)
|
119
|
+
# Offset field as returned by couch
|
120
|
+
attr_reader :offset
|
121
|
+
|
122
|
+
def initialize(array, offset)
|
123
|
+
@offset = offset
|
124
|
+
super(array)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
data/lib/throne/tasks.rb
CHANGED
@@ -1,52 +1,254 @@
|
|
1
1
|
# Defines rake tasks for managing databases and views.
|
2
|
+
#
|
3
|
+
# Requires either a SERVER_URL or DB_URL defined as an environment variable.
|
4
|
+
# If SERVER_URL is set the database path is inferred from the directory the files
|
5
|
+
# are stored in, if DB_URL is set that database is explicitly used,
|
6
|
+
# overriding the name of the folder/task.
|
2
7
|
class Throne::Tasks
|
3
|
-
|
8
|
+
# Injects rake tasks for loading and dumping data from databases.
|
9
|
+
# @param [String] base_path the path where design docs are stored
|
10
|
+
def self.inject_data_tasks(base_path)
|
4
11
|
namespace :throne do
|
5
|
-
namespace :
|
12
|
+
namespace :load_data do
|
6
13
|
dirs_in_path(base_path).each do |dir, fulldir|
|
7
|
-
desc "Loads
|
14
|
+
desc "Loads data for database #{dir}"
|
8
15
|
task dir.to_sym do
|
9
|
-
|
10
|
-
items = []
|
11
|
-
# grab and parse all YML files
|
12
|
-
Dir.glob(fulldir + '/**/*.yml').each do |yml|
|
13
|
-
items << YAML::load(File.open(yml))
|
14
|
-
end
|
15
|
-
# and json
|
16
|
-
Dir.glob(fulldir + '/**/*.json').each do |json|
|
17
|
-
items << JSON.parse(File.open(json).read)
|
18
|
-
end
|
19
|
-
# load em up
|
20
|
-
items.each do |item|
|
21
|
-
if item.kind_of? Array
|
22
|
-
item.each do |doc|
|
23
|
-
@db.save(doc)
|
24
|
-
end
|
25
|
-
elsif item.kind_of? Hash
|
26
|
-
@db.save(item)
|
27
|
-
else
|
28
|
-
puts "There is something funky with the data for #{dir}"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
p items[0]
|
16
|
+
load_data_for_database(url(dir), fulldir)
|
33
17
|
end
|
34
18
|
end
|
35
19
|
|
36
20
|
desc "load all dbs"
|
37
21
|
task :all => dirs_in_path(base_path)
|
38
22
|
end
|
23
|
+
namespace :dump_data do
|
24
|
+
desc "Dump data for a database into a YAML file"
|
25
|
+
task :yml, :db do |t, args|
|
26
|
+
dump_docs_from_db(args.db, base_path, :yml)
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Dump data for a database into a JSON file"
|
30
|
+
task :json, :db do |t, args|
|
31
|
+
dump_docs_from_db(args.db, base_path, :json)
|
32
|
+
end
|
33
|
+
end
|
39
34
|
end
|
40
35
|
end
|
41
36
|
|
37
|
+
# This will inject Rake tasks for loading design docs into the DB
|
38
|
+
# The docs should be layed out in the following format:
|
39
|
+
# base_path/
|
40
|
+
# |-- <db name>
|
41
|
+
# `-- <design doc name>
|
42
|
+
# |-- lists
|
43
|
+
# | `-- statuses
|
44
|
+
# | `-- list.js
|
45
|
+
# `-- views
|
46
|
+
# `-- statuses
|
47
|
+
# |-- map.js
|
48
|
+
# `-- reduce.js
|
49
|
+
#
|
50
|
+
# @param [String] base_path the path where design docs are stored
|
42
51
|
def self.inject_design_doc_tasks(base_path)
|
43
52
|
namespace :throne do
|
44
|
-
|
45
|
-
|
53
|
+
namespace :push_design do
|
54
|
+
dirs_in_path(base_path).each do |dir, fulldir|
|
55
|
+
desc "pushes designs for database #{dir}"
|
56
|
+
task dir.to_sym do
|
57
|
+
load_design_documents(url(dir), fulldir)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
desc "push all designs"
|
62
|
+
task :all => dirs_in_path(base_path)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Injects tasks to create and delete databases
|
68
|
+
def self.inject_database_tasks(base_path)
|
69
|
+
namespace :throne do
|
70
|
+
desc "Creates a database if it doesn't exist"
|
71
|
+
task :createdb, :db do |t, args|
|
72
|
+
get_db(args).create_database
|
73
|
+
puts "Database at #{url(args.db)}"
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "Deletes a database"
|
77
|
+
task :deletedb, :db do |t, args|
|
78
|
+
get_db(args).delete_database
|
79
|
+
puts "Database at #{url(args.db)}"
|
80
|
+
end
|
81
|
+
|
82
|
+
desc "Deletes, creates and re-loads a database"
|
83
|
+
task :rebuilddb, :db do |t, args|
|
84
|
+
db = get_db(args)
|
85
|
+
raise "you must specify the database name (task[db])" unless args.db
|
86
|
+
db.delete_database
|
87
|
+
# re-getting the object will create the DB
|
88
|
+
db = get_db(args)
|
89
|
+
# load design docs
|
90
|
+
load_design_documents(db.url, File.join(base_path, 'design', args.db))
|
91
|
+
# load data
|
92
|
+
load_data_for_database(db.url, File.join(base_path, 'data', args.db))
|
93
|
+
puts "Done."
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def self.get_db(args)
|
98
|
+
args ? db = args.db : db = nil
|
99
|
+
unless (ENV['SERVER_URL'] && args.db) || ENV['DB_URL']
|
100
|
+
raise "You must specify DB_URL or task[db_name] and SERVER_URL"
|
101
|
+
end
|
102
|
+
Throne::Database.new(url(db))
|
46
103
|
end
|
47
104
|
end
|
48
105
|
end
|
49
106
|
|
107
|
+
# Injects all tasks. Related data files will be stores in base_path/data, and
|
108
|
+
# design docs in base_path/design
|
109
|
+
#
|
110
|
+
# @param [String] base_path path for db related files.
|
111
|
+
def self.inject_all_tasks(base_path)
|
112
|
+
inject_data_tasks(File.join(base_path, 'data'))
|
113
|
+
inject_design_doc_tasks(File.join(base_path, 'design'))
|
114
|
+
inject_database_tasks(base_path)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Loads design documents into the database url, extracted from the source path
|
118
|
+
# The docs should be layed out in the following format:
|
119
|
+
# base_path/
|
120
|
+
# |-- <db name>
|
121
|
+
# `-- <design doc name>
|
122
|
+
# |-- lists
|
123
|
+
# | `-- statuses
|
124
|
+
# | `-- list.js
|
125
|
+
# `-- views
|
126
|
+
# `-- statuses
|
127
|
+
# |-- map.js
|
128
|
+
# `-- reduce.js
|
129
|
+
# @param [String] db_url the url of the database to load the data in to
|
130
|
+
# @param [String] source_path the path to search for .yml and .json files
|
131
|
+
def self.load_design_documents(db_url, source_path)
|
132
|
+
# for each folder in base path, create a new design doc key
|
133
|
+
# create a lists key
|
134
|
+
# for each path in lists, add a key with the folder name
|
135
|
+
# inside this, there is a key called list, with the contents of the list function
|
136
|
+
# views is the same, except with a map and reduce function
|
137
|
+
Dir.glob(File.join(source_path, '*')).each do |doc_path|
|
138
|
+
doc_name = File.basename(doc_path)
|
139
|
+
doc = {'lists' => {}, 'views' => {}}
|
140
|
+
Dir.glob(File.join(doc_path, 'lists', '*')) do |list_path|
|
141
|
+
list_name = File.basename(list_path)
|
142
|
+
doc['lists'][list_name] = {}
|
143
|
+
listfn = File.join(list_path, 'list.js')
|
144
|
+
doc['lists'][list_name] =
|
145
|
+
File.read(listfn) if File.exists?(listfn)
|
146
|
+
end
|
147
|
+
Dir.glob(File.join(doc_path, 'views', '*')) do |view_path|
|
148
|
+
view_name = File.basename(view_path)
|
149
|
+
doc['views'][view_name] = {}
|
150
|
+
mapfn = File.join(view_path, 'map.js')
|
151
|
+
reducefn = File.join(view_path, 'reduce.js')
|
152
|
+
doc['views'][view_name]['map'] =
|
153
|
+
File.read(mapfn) if File.exists?(mapfn)
|
154
|
+
doc['views'][view_name]['reduce'] =
|
155
|
+
File.read(reducefn) if File.exists?(reducefn)
|
156
|
+
end
|
157
|
+
# try to get the existing doc
|
158
|
+
doc_id = "_design/#{doc_name}"
|
159
|
+
db = Throne::Database.new(db_url)
|
160
|
+
if svr_doc = db.get(doc_id)
|
161
|
+
# merge
|
162
|
+
doc = svr_doc.merge(doc)
|
163
|
+
else
|
164
|
+
doc['_id'] = doc_id
|
165
|
+
end
|
166
|
+
doc['language'] = 'javascript'
|
167
|
+
db.save(doc)
|
168
|
+
puts "Design documents from #{source_path} loaded"
|
169
|
+
end
|
170
|
+
|
171
|
+
# try and get a document with the design name
|
172
|
+
# if it's there, replace the lists and views keys with above data
|
173
|
+
# otherwise, create a new document, set language to javascript
|
174
|
+
# put document.
|
175
|
+
# WIN
|
176
|
+
end
|
177
|
+
|
178
|
+
# Loads data into the database url from the source path. Picks up .yml and .json
|
179
|
+
# @param [String] db_url the url of the database to load the data in to
|
180
|
+
# @param [String] source_path the path to search for .yml and .json files
|
181
|
+
def self.load_data_for_database(db_url, source_path)
|
182
|
+
@db = Throne::Database.new(db_url)
|
183
|
+
items = []
|
184
|
+
doccount = 0
|
185
|
+
# grab and parse all YML files
|
186
|
+
Dir.glob(source_path + '/**/*.yml').each do |yml|
|
187
|
+
items << YAML::load(File.open(yml))
|
188
|
+
end
|
189
|
+
# and json
|
190
|
+
Dir.glob(source_path + '/**/*.json').each do |json|
|
191
|
+
items << JSON.parse(File.open(json).read)
|
192
|
+
end
|
193
|
+
# load em up
|
194
|
+
items.each do |item|
|
195
|
+
if item.kind_of? Array
|
196
|
+
item.each do |doc|
|
197
|
+
begin
|
198
|
+
@db.save(doc)
|
199
|
+
rescue RestClient::RequestFailed => e
|
200
|
+
if e.message =~ /409$/
|
201
|
+
puts "Duplicate document - this data has probaby already been loaded"
|
202
|
+
doccount -= 1
|
203
|
+
else
|
204
|
+
raise e
|
205
|
+
end
|
206
|
+
end
|
207
|
+
doccount += 1
|
208
|
+
end
|
209
|
+
elsif item.kind_of? Hash
|
210
|
+
begin
|
211
|
+
@db.save(item)
|
212
|
+
rescue RestClient::RequestFailed => e
|
213
|
+
if e.message =~ /409$/
|
214
|
+
puts "Duplicate document - this data has probaby already been loaded"
|
215
|
+
doccount -= 1
|
216
|
+
else
|
217
|
+
raise e
|
218
|
+
end
|
219
|
+
end
|
220
|
+
doccount += 1
|
221
|
+
else
|
222
|
+
puts "There is something funky with the data for #{source_path}"
|
223
|
+
end
|
224
|
+
puts "#{doccount} document(s) loaded into database at #{@db.url}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def self.dump_docs_from_db(db, base_path, format)
|
229
|
+
raise "You must specify a DB name to dump task[dbname]" unless db
|
230
|
+
outdir = File.join(base_path, db)
|
231
|
+
Dir.mkdir(outdir) unless File.exists?(outdir)
|
232
|
+
@db = Throne::Database.new(url(db))
|
233
|
+
docs = []
|
234
|
+
|
235
|
+
@db.function('_all_docs') do |res|
|
236
|
+
docs << @db.get(res['key']) unless res['key'].match(/^_/)
|
237
|
+
end
|
238
|
+
|
239
|
+
outfn = format == :json ? 'dump.json' : 'dump.yml'
|
240
|
+
File.open(File.join(outdir, outfn), 'w') do |f|
|
241
|
+
if format == :json
|
242
|
+
f.puts docs.to_json
|
243
|
+
elsif format == :yml
|
244
|
+
f.puts docs.to_yaml
|
245
|
+
else
|
246
|
+
raise("Internal Error - invalid dump format specified")
|
247
|
+
end
|
248
|
+
end
|
249
|
+
puts "Data dumped to #{base_path + '/' + db + '/' + outfn}"
|
250
|
+
end
|
251
|
+
|
50
252
|
private
|
51
253
|
|
52
254
|
def self.dirs_in_path(path)
|
@@ -63,7 +265,8 @@ class Throne::Tasks
|
|
63
265
|
elsif svrurl = ENV['SERVER_URL']
|
64
266
|
return svrurl + '/' + dbname
|
65
267
|
else
|
66
|
-
raise("You must provide a SERVER_URL or
|
268
|
+
raise("You must provide a SERVER_URL or DB_URL")
|
67
269
|
end
|
68
270
|
end
|
271
|
+
|
69
272
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: throne
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lincoln Stoll
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-12-
|
12
|
+
date: 2009-12-14 00:00:00 +11:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -60,14 +60,15 @@ extensions: []
|
|
60
60
|
|
61
61
|
extra_rdoc_files:
|
62
62
|
- LICENSE
|
63
|
-
- README.
|
63
|
+
- README.rdoc
|
64
|
+
- TODO
|
64
65
|
files:
|
65
66
|
- .document
|
66
67
|
- .gitignore
|
67
|
-
- FEATURES
|
68
68
|
- LICENSE
|
69
|
-
- README.
|
69
|
+
- README.rdoc
|
70
70
|
- Rakefile
|
71
|
+
- TODO
|
71
72
|
- VERSION
|
72
73
|
- lib/throne.rb
|
73
74
|
- lib/throne/database.rb
|