relaxdb 0.3.5
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.textile +200 -0
- data/Rakefile +63 -0
- data/docs/spec_results.html +1059 -0
- data/lib/more/atomic_bulk_save_support.rb +18 -0
- data/lib/more/grapher.rb +48 -0
- data/lib/relaxdb.rb +50 -0
- data/lib/relaxdb/all_delegator.rb +44 -0
- data/lib/relaxdb/belongs_to_proxy.rb +29 -0
- data/lib/relaxdb/design_doc.rb +57 -0
- data/lib/relaxdb/document.rb +600 -0
- data/lib/relaxdb/extlib.rb +24 -0
- data/lib/relaxdb/has_many_proxy.rb +101 -0
- data/lib/relaxdb/has_one_proxy.rb +42 -0
- data/lib/relaxdb/migration.rb +40 -0
- data/lib/relaxdb/migration_version.rb +21 -0
- data/lib/relaxdb/net_http_server.rb +61 -0
- data/lib/relaxdb/paginate_params.rb +53 -0
- data/lib/relaxdb/paginator.rb +88 -0
- data/lib/relaxdb/query.rb +76 -0
- data/lib/relaxdb/references_many_proxy.rb +97 -0
- data/lib/relaxdb/relaxdb.rb +250 -0
- data/lib/relaxdb/server.rb +109 -0
- data/lib/relaxdb/taf2_curb_server.rb +63 -0
- data/lib/relaxdb/uuid_generator.rb +21 -0
- data/lib/relaxdb/validators.rb +11 -0
- data/lib/relaxdb/view_object.rb +34 -0
- data/lib/relaxdb/view_result.rb +18 -0
- data/lib/relaxdb/view_uploader.rb +49 -0
- data/lib/relaxdb/views.rb +114 -0
- data/readme.rb +80 -0
- data/spec/belongs_to_spec.rb +124 -0
- data/spec/callbacks_spec.rb +80 -0
- data/spec/derived_properties_spec.rb +112 -0
- data/spec/design_doc_spec.rb +34 -0
- data/spec/doc_inheritable_spec.rb +100 -0
- data/spec/document_spec.rb +545 -0
- data/spec/has_many_spec.rb +202 -0
- data/spec/has_one_spec.rb +123 -0
- data/spec/migration_spec.rb +97 -0
- data/spec/migration_version_spec.rb +28 -0
- data/spec/paginate_params_spec.rb +15 -0
- data/spec/paginate_spec.rb +360 -0
- data/spec/query_spec.rb +90 -0
- data/spec/references_many_spec.rb +173 -0
- data/spec/relaxdb_spec.rb +364 -0
- data/spec/server_spec.rb +32 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/spec_models.rb +199 -0
- data/spec/view_by_spec.rb +76 -0
- data/spec/view_object_spec.rb +47 -0
- data/spec/view_spec.rb +23 -0
- metadata +137 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
class Errors < Hash
|
2
|
+
alias_method :on, :[]
|
3
|
+
alias_method :count, :size
|
4
|
+
|
5
|
+
def full_messages
|
6
|
+
full_messages = []
|
7
|
+
each_key do |attr|
|
8
|
+
full_messages << attr.to_s + ": " + self[attr]
|
9
|
+
end
|
10
|
+
full_messages
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Time
|
15
|
+
|
16
|
+
# Ensure that all Times are stored as UTC
|
17
|
+
# Times in the following format may be passed directly to
|
18
|
+
# Date.new in a JavaScript runtime
|
19
|
+
def to_json
|
20
|
+
utc
|
21
|
+
%Q("#{strftime "%Y/%m/%d %H:%M:%S +0000"}")
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class HasManyProxy
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :children
|
8
|
+
|
9
|
+
def initialize(client, relationship, opts)
|
10
|
+
@client = client
|
11
|
+
@relationship = relationship
|
12
|
+
@opts = opts
|
13
|
+
|
14
|
+
@target_class = opts[:class]
|
15
|
+
@relationship_as_viewed_by_target = (opts[:known_as] || client.class.name.snake_case).to_s
|
16
|
+
|
17
|
+
@children = load_children
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(obj)
|
21
|
+
return false if @children.include?(obj)
|
22
|
+
|
23
|
+
obj.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
|
24
|
+
if obj.save
|
25
|
+
@children << obj
|
26
|
+
self
|
27
|
+
else
|
28
|
+
false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
@children.each do |c|
|
34
|
+
break_back_link c
|
35
|
+
end
|
36
|
+
@children.clear
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete(obj)
|
40
|
+
obj = @children.delete(obj)
|
41
|
+
break_back_link(obj) if obj
|
42
|
+
end
|
43
|
+
|
44
|
+
def break_back_link(obj)
|
45
|
+
if obj
|
46
|
+
obj.send("#{@relationship_as_viewed_by_target}=".to_sym, nil)
|
47
|
+
obj.save
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def empty?
|
52
|
+
@children.empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
def size
|
56
|
+
@children.size
|
57
|
+
end
|
58
|
+
|
59
|
+
def [](*args)
|
60
|
+
@children[*args]
|
61
|
+
end
|
62
|
+
|
63
|
+
def first
|
64
|
+
@children[0]
|
65
|
+
end
|
66
|
+
|
67
|
+
def last
|
68
|
+
@children[size-1]
|
69
|
+
end
|
70
|
+
|
71
|
+
def each(&blk)
|
72
|
+
@children.each(&blk)
|
73
|
+
end
|
74
|
+
|
75
|
+
def reload
|
76
|
+
@children = load_children
|
77
|
+
end
|
78
|
+
|
79
|
+
def load_children
|
80
|
+
view_name = "#{@client.class}_#{@relationship}"
|
81
|
+
@children = RelaxDB.view(view_name, :key => @client._id)
|
82
|
+
end
|
83
|
+
|
84
|
+
def children=(children)
|
85
|
+
children.each do |obj|
|
86
|
+
obj.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
|
87
|
+
end
|
88
|
+
@children = children
|
89
|
+
end
|
90
|
+
|
91
|
+
def inspect
|
92
|
+
@children.inspect
|
93
|
+
end
|
94
|
+
|
95
|
+
# Play nice with Merb partials - [ obj ].flatten invokes
|
96
|
+
# obj.to_ary if it responds to to_ary
|
97
|
+
alias_method :to_ary, :to_a
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class HasOneProxy
|
4
|
+
|
5
|
+
def initialize(client, relationship)
|
6
|
+
@client = client
|
7
|
+
@relationship = relationship
|
8
|
+
@target_class = @relationship.to_s.camel_case
|
9
|
+
@relationship_as_viewed_by_target = client.class.name.snake_case
|
10
|
+
|
11
|
+
@target = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def target
|
15
|
+
return @target if @target
|
16
|
+
@target = load_target
|
17
|
+
end
|
18
|
+
|
19
|
+
# All database changes performed by this method would ideally be done in a transaction
|
20
|
+
def target=(new_target)
|
21
|
+
# Nullify any existing relationship on assignment
|
22
|
+
old_target = target
|
23
|
+
if old_target
|
24
|
+
old_target.send("#{@relationship_as_viewed_by_target}=".to_sym, nil)
|
25
|
+
old_target.save
|
26
|
+
end
|
27
|
+
|
28
|
+
@target = new_target
|
29
|
+
unless @target.nil?
|
30
|
+
@target.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
|
31
|
+
@target.save
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_target
|
36
|
+
view_name = "#{@client.class}_#{@relationship}"
|
37
|
+
RelaxDB.view(view_name, :key => @client._id).first
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class Migration
|
4
|
+
|
5
|
+
def self.run klass, limit = 1000
|
6
|
+
query = lambda do |page_params|
|
7
|
+
RelaxDB.paginate_view "#{klass}_all", :startkey => nil, :endkey => {}, :attributes => [:_id],
|
8
|
+
:page_params => page_params, :limit => limit
|
9
|
+
end
|
10
|
+
|
11
|
+
objs = query.call({})
|
12
|
+
until objs.empty?
|
13
|
+
migrated = objs.map { |o| yield o }.flatten.reject { |o| o.nil? }
|
14
|
+
RelaxDB.bulk_save! *migrated
|
15
|
+
objs = objs.next_params ? query.call(objs.next_params) : []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Runs all outstanding migrations in a given directory
|
21
|
+
#
|
22
|
+
# ==== Example
|
23
|
+
# RelaxDB::Migration.run_all Dir["couchdb/migrations/**/*.rb"]
|
24
|
+
#
|
25
|
+
def self.run_all file_names, action = lambda { |fn| require fn }
|
26
|
+
v = RelaxDB::MigrationVersion.version
|
27
|
+
file_names.select { |fn| fv(fn) > v }.each do |fn|
|
28
|
+
RelaxDB.logger.info "Applying #{fn}"
|
29
|
+
action.call fn
|
30
|
+
RelaxDB::MigrationVersion.update fv(fn)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.fv file_name
|
35
|
+
File.basename(file_name).split("_")[0].to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class RelaxDB::MigrationVersion < RelaxDB::Document
|
2
|
+
|
3
|
+
DOC_ID = "relaxdb_migration_version"
|
4
|
+
|
5
|
+
property :version, :default => 0
|
6
|
+
|
7
|
+
def self.version
|
8
|
+
retrieve.version
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.update v
|
12
|
+
mv = retrieve
|
13
|
+
mv.version = v
|
14
|
+
mv.save!
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.retrieve
|
18
|
+
(v = RelaxDB.load(DOC_ID)) ? v : new(:_id => DOC_ID).save!
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class Server
|
4
|
+
|
5
|
+
def initialize(host, port)
|
6
|
+
@host = host
|
7
|
+
@port = port
|
8
|
+
end
|
9
|
+
|
10
|
+
def delete(uri)
|
11
|
+
request(Net::HTTP::Delete.new(uri))
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(uri)
|
15
|
+
request(Net::HTTP::Get.new(uri))
|
16
|
+
end
|
17
|
+
|
18
|
+
def put(uri, json)
|
19
|
+
req = Net::HTTP::Put.new(uri)
|
20
|
+
req["content-type"] = "application/json"
|
21
|
+
req.body = json
|
22
|
+
request(req)
|
23
|
+
end
|
24
|
+
|
25
|
+
def post(uri, json)
|
26
|
+
req = Net::HTTP::Post.new(uri)
|
27
|
+
req["content-type"] = "application/json"
|
28
|
+
req.body = json
|
29
|
+
request(req)
|
30
|
+
end
|
31
|
+
|
32
|
+
def request(req)
|
33
|
+
res = Net::HTTP.start(@host, @port) {|http|
|
34
|
+
http.request(req)
|
35
|
+
}
|
36
|
+
if (not res.kind_of?(Net::HTTPSuccess))
|
37
|
+
handle_error(req, res)
|
38
|
+
end
|
39
|
+
res
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
"http://#{@host}:#{@port}/"
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def handle_error(req, res)
|
49
|
+
msg = "#{res.code}:#{res.message}\nMETHOD:#{req.method}\nURI:#{req.path}\n#{res.body}"
|
50
|
+
begin
|
51
|
+
klass = RelaxDB.const_get("HTTP_#{res.code}")
|
52
|
+
e = klass.new(msg)
|
53
|
+
rescue
|
54
|
+
e = RuntimeError.new(msg)
|
55
|
+
end
|
56
|
+
|
57
|
+
raise e
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class PaginateParams
|
4
|
+
|
5
|
+
@@params = %w(key startkey startkey_docid endkey endkey_docid limit update descending group reduce include_docs)
|
6
|
+
|
7
|
+
@@params.each do |param|
|
8
|
+
define_method(param.to_sym) do |*val|
|
9
|
+
if val.empty?
|
10
|
+
instance_variable_get("@#{param}")
|
11
|
+
else
|
12
|
+
instance_variable_set("@#{param}", val[0])
|
13
|
+
# null is meaningful to CouchDB. _set allows us to know that a param has been set, even to nil
|
14
|
+
instance_variable_set("@#{param}_set", true)
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(params)
|
21
|
+
params.each { |k, v| send(k, v) }
|
22
|
+
|
23
|
+
# If a client hasn't explicitly set descending, set it to the CouchDB default
|
24
|
+
@descending = false if @descending.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def update(params)
|
28
|
+
@order_inverted = params[:descending].nil? ? false : @descending ^ params[:descending]
|
29
|
+
@descending = !@descending if @order_inverted
|
30
|
+
|
31
|
+
@endkey = @startkey if @order_inverted
|
32
|
+
|
33
|
+
@startkey = params[:startkey] || @startkey
|
34
|
+
|
35
|
+
@skip = 1 if params[:startkey]
|
36
|
+
|
37
|
+
@startkey_docid = params[:startkey_docid] if params[:startkey_docid]
|
38
|
+
@endkey_docid = params[:endkey_docid] if params[:endkey_docid]
|
39
|
+
end
|
40
|
+
|
41
|
+
def order_inverted?
|
42
|
+
@order_inverted
|
43
|
+
end
|
44
|
+
|
45
|
+
def invalid?
|
46
|
+
# Simply because allowing either to be omitted increases the complexity of the paginator
|
47
|
+
@startkey_set && @endkey_set ? nil : "Both startkey and endkey must be set"
|
48
|
+
end
|
49
|
+
alias error_msg invalid?
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class Paginator
|
4
|
+
|
5
|
+
attr_reader :paginate_params
|
6
|
+
|
7
|
+
def initialize(paginate_params, page_params)
|
8
|
+
@paginate_params = paginate_params
|
9
|
+
@orig_paginate_params = @paginate_params.clone
|
10
|
+
|
11
|
+
page_params = page_params.is_a?(String) ? JSON.parse(page_params).to_mash : page_params
|
12
|
+
# Where the magic happens - the original params are updated with the page specific params
|
13
|
+
@paginate_params.update(page_params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def total_doc_count(view_name)
|
17
|
+
RelaxDB.view view_name, :startkey => @orig_paginate_params.startkey, :endkey => @orig_paginate_params.endkey,
|
18
|
+
:descending => @orig_paginate_params.descending, :reduce => true
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# view_keys are used to determine the params for the prev and next links. If a view_key is a symbol
|
23
|
+
# the key value will be the result of invoking the method named by the symbol on the first / last
|
24
|
+
# element in the result set. If a view_key is not a symbol, its value will be used directly.
|
25
|
+
#
|
26
|
+
def add_next_and_prev(docs, view_name, view_keys)
|
27
|
+
unless docs.empty?
|
28
|
+
no_docs = docs.size
|
29
|
+
offset = docs.offset
|
30
|
+
orig_offset = orig_offset(view_name)
|
31
|
+
total_doc_count = total_doc_count(view_name)
|
32
|
+
|
33
|
+
next_exists = !@paginate_params.order_inverted? ? (offset - orig_offset + no_docs < total_doc_count) : true
|
34
|
+
next_params = create_next(docs.last, view_keys) if next_exists
|
35
|
+
|
36
|
+
prev_exists = @paginate_params.order_inverted? ? (offset - orig_offset + no_docs < total_doc_count) :
|
37
|
+
(offset - orig_offset == 0 ? false : true)
|
38
|
+
prev_params = create_prev(docs.first, view_keys) if prev_exists
|
39
|
+
else
|
40
|
+
next_exists = prev_exists = false
|
41
|
+
end
|
42
|
+
|
43
|
+
docs.meta_class.instance_eval do
|
44
|
+
define_method(:next_params) { next_exists ? next_params : false }
|
45
|
+
define_method(:next_query) { next_exists ? "page_params=#{::CGI::escape(next_params.to_json)}" : false }
|
46
|
+
|
47
|
+
define_method(:prev_params) { prev_exists ? prev_params : false }
|
48
|
+
define_method(:prev_query) { prev_exists ? "page_params=#{::CGI::escape(prev_params.to_json)}" : false }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_next(doc, view_keys)
|
53
|
+
next_key = view_keys_to_vals doc, view_keys
|
54
|
+
next_key = next_key.length == 1 ? next_key[0] : next_key
|
55
|
+
next_key_docid = doc._id
|
56
|
+
{ :startkey => next_key, :startkey_docid => next_key_docid, :descending => @orig_paginate_params.descending }
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_prev(doc, view_keys)
|
60
|
+
prev_key = view_keys_to_vals doc, view_keys
|
61
|
+
prev_key = prev_key.length == 1 ? prev_key[0] : prev_key
|
62
|
+
prev_key_docid = doc._id
|
63
|
+
prev_params = { :startkey => prev_key, :startkey_docid => prev_key_docid, :descending => !@orig_paginate_params.descending }
|
64
|
+
end
|
65
|
+
|
66
|
+
def view_keys_to_vals doc, view_keys
|
67
|
+
view_keys.map do |k|
|
68
|
+
if k.is_a?(Symbol) then doc.send(k)
|
69
|
+
elsif k.is_a?(Proc) then k.call(doc)
|
70
|
+
else k
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def orig_offset(view_name)
|
76
|
+
if @paginate_params.order_inverted?
|
77
|
+
params = {:startkey => @orig_paginate_params.endkey, :descending => !@orig_paginate_params.descending}
|
78
|
+
else
|
79
|
+
params = {:startkey => @orig_paginate_params.startkey, :descending => @orig_paginate_params.descending}
|
80
|
+
end
|
81
|
+
params[:limit] = 1
|
82
|
+
|
83
|
+
RelaxDB.rf_view(view_name, params).offset
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
# A Query is used to build the query string made against a view
|
4
|
+
# All parameter values are first JSON encoded and then URL encoded
|
5
|
+
# Nil values are set to the empty string
|
6
|
+
|
7
|
+
class Query
|
8
|
+
|
9
|
+
# keys is not included in the standard param as it is significantly different from the others
|
10
|
+
@@params = %w(key startkey startkey_docid endkey endkey_docid limit update
|
11
|
+
descending skip group group_level reduce include_docs batch)
|
12
|
+
|
13
|
+
@@params.each do |param|
|
14
|
+
define_method(param.to_sym) do |*val|
|
15
|
+
if val.empty?
|
16
|
+
instance_variable_get("@#{param}")
|
17
|
+
else
|
18
|
+
instance_variable_set("@#{param}", val[0])
|
19
|
+
# null is meaningful to CouchDB. _set allows us to know that a param has been set, even to nil
|
20
|
+
instance_variable_set("@#{param}_set", true)
|
21
|
+
self
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize view_name, params = {}
|
27
|
+
@view_name = view_name
|
28
|
+
params.each { |k, v| send k, v }
|
29
|
+
end
|
30
|
+
|
31
|
+
def keys(keys=nil)
|
32
|
+
if keys.nil?
|
33
|
+
@keys
|
34
|
+
else
|
35
|
+
@keys = { :keys => keys }.to_json
|
36
|
+
self
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# If set to true RelaxDB.view will return unprocessed data
|
41
|
+
def raw(val = nil)
|
42
|
+
if val.nil?
|
43
|
+
@raw
|
44
|
+
else
|
45
|
+
@raw = val
|
46
|
+
self
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def view_path
|
51
|
+
uri = (@view_name =~ /^_/) ? @view_name : "_design/#{RelaxDB.dd}/_view/#{@view_name}"
|
52
|
+
|
53
|
+
query = ""
|
54
|
+
@@params.each do |param|
|
55
|
+
val_set = instance_variable_get("@#{param}_set")
|
56
|
+
if val_set
|
57
|
+
val = instance_variable_get("@#{param}")
|
58
|
+
val = val.to_json unless ["startkey_docid", "endkey_docid"].include?(param)
|
59
|
+
query << "&#{param}=#{::CGI::escape(val)}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
uri << query.sub(/^&/, "?")
|
64
|
+
end
|
65
|
+
|
66
|
+
def merge(paginate_params)
|
67
|
+
paginate_params.instance_variables.each do |pp|
|
68
|
+
val = paginate_params.instance_variable_get(pp)
|
69
|
+
method_name = pp[1, pp.length]
|
70
|
+
send(method_name, val) if methods.include? method_name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|