ceritium-relaxdb 0.2.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.textile +185 -0
- data/Rakefile +58 -0
- data/docs/spec_results.html +604 -0
- data/lib/more/atomic_bulk_save_support.rb +18 -0
- data/lib/more/grapher.rb +48 -0
- data/lib/relaxdb.rb +40 -0
- data/lib/relaxdb/all_delegator.rb +68 -0
- data/lib/relaxdb/belongs_to_proxy.rb +29 -0
- data/lib/relaxdb/design_doc.rb +55 -0
- data/lib/relaxdb/document.rb +531 -0
- data/lib/relaxdb/extlib.rb +15 -0
- data/lib/relaxdb/has_many_proxy.rb +104 -0
- data/lib/relaxdb/has_one_proxy.rb +45 -0
- data/lib/relaxdb/paginate_params.rb +54 -0
- data/lib/relaxdb/paginator.rb +78 -0
- data/lib/relaxdb/query.rb +75 -0
- data/lib/relaxdb/references_many_proxy.rb +101 -0
- data/lib/relaxdb/relaxdb.rb +208 -0
- data/lib/relaxdb/server.rb +156 -0
- data/lib/relaxdb/sorted_by_view.rb +65 -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 +47 -0
- data/lib/relaxdb/views.rb +54 -0
- data/spec/belongs_to_spec.rb +129 -0
- data/spec/callbacks_spec.rb +80 -0
- data/spec/derived_properties_spec.rb +117 -0
- data/spec/design_doc_spec.rb +34 -0
- data/spec/document_spec.rb +556 -0
- data/spec/has_many_spec.rb +176 -0
- data/spec/has_one_spec.rb +128 -0
- data/spec/query_spec.rb +80 -0
- data/spec/references_many_spec.rb +178 -0
- data/spec/relaxdb_spec.rb +226 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/spec_models.rb +151 -0
- data/spec/view_object_spec.rb +47 -0
- metadata +123 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
class Errors < Hash
|
2
|
+
alias_method :on, :[]
|
3
|
+
end
|
4
|
+
|
5
|
+
class Time
|
6
|
+
|
7
|
+
# Ensure that all Times are stored as UTC
|
8
|
+
# Times in the following format may be passed directly to
|
9
|
+
# Date.new in a JavaScript runtime
|
10
|
+
def to_json
|
11
|
+
utc
|
12
|
+
%Q("#{strftime "%Y/%m/%d %H:%M:%S +0000"}")
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,104 @@
|
|
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_path = "_design/#{@client.class}/_view/#{@relationship}?key=\"#{@client._id}\""
|
81
|
+
design_doc = @client.class
|
82
|
+
view_name = @relationship
|
83
|
+
map_function = ViewCreator.has_n(@target_class, @relationship_as_viewed_by_target)
|
84
|
+
@children = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
|
85
|
+
end
|
86
|
+
|
87
|
+
def children=(children)
|
88
|
+
children.each do |obj|
|
89
|
+
obj.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
|
90
|
+
end
|
91
|
+
@children = children
|
92
|
+
end
|
93
|
+
|
94
|
+
def inspect
|
95
|
+
@children.inspect
|
96
|
+
end
|
97
|
+
|
98
|
+
# Play nice with Merb partials - [ obj ].flatten invokes
|
99
|
+
# obj.to_ary if it responds to to_ary
|
100
|
+
alias_method :to_ary, :to_a
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
@@ -0,0 +1,45 @@
|
|
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
|
+
design_doc = @client.class
|
37
|
+
view_name = @relationship
|
38
|
+
view_path = "_design/#{design_doc}/_view/#{view_name}?key=\"#{@client._id}\""
|
39
|
+
map_function = ViewCreator.has_n(@target_class, @relationship_as_viewed_by_target)
|
40
|
+
RelaxDB.retrieve(view_path, design_doc, view_name, map_function).first
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,54 @@
|
|
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
|
21
|
+
# If a client hasn't explicitly set descending, set it to the CouchDB default
|
22
|
+
@descending = false if @descending.nil?
|
23
|
+
# CouchDB defaults reduce to true when a reduce func is present
|
24
|
+
@reduce = false
|
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
|
+
# This constraint may be removed in future, but don't hold your breath
|
48
|
+
@startkey_set && @endkey_set ? nil : "Both startkey and endkey must be set"
|
49
|
+
end
|
50
|
+
alias error_msg invalid?
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,78 @@
|
|
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(design_doc, view_name)
|
17
|
+
result = RelaxDB.view(design_doc, view_name) do |q|
|
18
|
+
q.reduce(true)
|
19
|
+
q.startkey(@orig_paginate_params.startkey).endkey(@orig_paginate_params.endkey).descending(@orig_paginate_params.descending)
|
20
|
+
end
|
21
|
+
|
22
|
+
total_docs = RelaxDB.reduce_result(result)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_next_and_prev(docs, design_doc, view_name, view_keys)
|
26
|
+
unless docs.empty?
|
27
|
+
no_docs = docs.size
|
28
|
+
offset = docs.offset
|
29
|
+
orig_offset = orig_offset(design_doc, view_name)
|
30
|
+
total_doc_count = total_doc_count(design_doc, view_name)
|
31
|
+
|
32
|
+
next_exists = !@paginate_params.order_inverted? ? (offset - orig_offset + no_docs < total_doc_count) : true
|
33
|
+
next_params = create_next(docs.last, view_keys) if next_exists
|
34
|
+
|
35
|
+
prev_exists = @paginate_params.order_inverted? ? (offset - orig_offset + no_docs < total_doc_count) :
|
36
|
+
(offset - orig_offset == 0 ? false : true)
|
37
|
+
prev_params = create_prev(docs.first, view_keys) if prev_exists
|
38
|
+
else
|
39
|
+
next_exists = prev_exists = false
|
40
|
+
end
|
41
|
+
|
42
|
+
docs.meta_class.instance_eval do
|
43
|
+
define_method(:next_params) { next_exists ? next_params : false }
|
44
|
+
define_method(:next_query) { next_exists ? "page_params=#{::CGI::escape(next_params.to_json)}" : false }
|
45
|
+
|
46
|
+
define_method(:prev_params) { prev_exists ? prev_params : false }
|
47
|
+
define_method(:prev_query) { prev_exists ? "page_params=#{::CGI::escape(prev_params.to_json)}" : false }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_next(doc, view_keys)
|
52
|
+
next_key = view_keys.map { |a| doc.send(a) }
|
53
|
+
next_key = next_key.length == 1 ? next_key[0] : next_key
|
54
|
+
next_key_docid = doc._id
|
55
|
+
{ :startkey => next_key, :startkey_docid => next_key_docid, :descending => @orig_paginate_params.descending }
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_prev(doc, view_keys)
|
59
|
+
prev_key = view_keys.map { |a| doc.send(a) }
|
60
|
+
prev_key = prev_key.length == 1 ? prev_key[0] : prev_key
|
61
|
+
prev_key_docid = doc._id
|
62
|
+
prev_params = { :startkey => prev_key, :startkey_docid => prev_key_docid, :descending => !@orig_paginate_params.descending }
|
63
|
+
end
|
64
|
+
|
65
|
+
def orig_offset(design_doc, view_name)
|
66
|
+
query = Query.new(design_doc, view_name)
|
67
|
+
if @paginate_params.order_inverted?
|
68
|
+
query.startkey(@orig_paginate_params.endkey).descending(!@orig_paginate_params.descending)
|
69
|
+
else
|
70
|
+
query.startkey(@orig_paginate_params.startkey).descending(@orig_paginate_params.descending)
|
71
|
+
end
|
72
|
+
query.reduce(false).limit(1)
|
73
|
+
RelaxDB.retrieve(query.view_path).offset
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,75 @@
|
|
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
|
+
# All parameter calls return self so calls may be chained => q.startkey("foo").endkey("bar").limit(2)
|
7
|
+
|
8
|
+
#
|
9
|
+
# The query object is currently inconsistent with the RelaxDB object idiom. Consider
|
10
|
+
# paul = User.new(:name => "paul").save; Event.new(:host=>paul).save
|
11
|
+
# but an event query requires
|
12
|
+
# Event.all.sorted_by(:host_id) { |q| q.key(paul._id) }
|
13
|
+
# rather than
|
14
|
+
# Event.all.sorted_by(:host) { |q| q.key(paul) }
|
15
|
+
# I feel that both forms should be supported
|
16
|
+
#
|
17
|
+
class Query
|
18
|
+
|
19
|
+
# keys is not included in the standard param as it is significantly different from the others
|
20
|
+
@@params = %w(key startkey startkey_docid endkey endkey_docid limit update descending skip group group_level reduce include_docs)
|
21
|
+
|
22
|
+
@@params.each do |param|
|
23
|
+
define_method(param.to_sym) do |*val|
|
24
|
+
if val.empty?
|
25
|
+
instance_variable_get("@#{param}")
|
26
|
+
else
|
27
|
+
instance_variable_set("@#{param}", val[0])
|
28
|
+
# null is meaningful to CouchDB. _set allows us to know that a param has been set, even to nil
|
29
|
+
instance_variable_set("@#{param}_set", true)
|
30
|
+
self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(design_doc, view_name)
|
36
|
+
@design_doc = design_doc
|
37
|
+
@view_name = view_name
|
38
|
+
end
|
39
|
+
|
40
|
+
def keys(keys=nil)
|
41
|
+
if keys.nil?
|
42
|
+
@keys
|
43
|
+
else
|
44
|
+
@keys = { :keys => keys }.to_json
|
45
|
+
self
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def view_path
|
50
|
+
uri = "_design/#{@design_doc}/_view/#{@view_name}"
|
51
|
+
|
52
|
+
query = ""
|
53
|
+
@@params.each do |param|
|
54
|
+
val_set = instance_variable_get("@#{param}_set")
|
55
|
+
if val_set
|
56
|
+
val = instance_variable_get("@#{param}")
|
57
|
+
val = val.to_json unless ["startkey_docid", "endkey_docid"].include?(param)
|
58
|
+
query << "&#{param}=#{::CGI::escape(val)}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
uri << query.sub(/^&/, "?")
|
63
|
+
end
|
64
|
+
|
65
|
+
def merge(paginate_params)
|
66
|
+
paginate_params.instance_variables.each do |pp|
|
67
|
+
val = paginate_params.instance_variable_get(pp)
|
68
|
+
method_name = pp[1, pp.length]
|
69
|
+
send(method_name, val) if methods.include? method_name
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class ReferencesManyProxy
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(client, relationship, opts)
|
8
|
+
@client = client
|
9
|
+
@relationship = relationship
|
10
|
+
|
11
|
+
@target_class = opts[:class]
|
12
|
+
@relationship_as_viewed_by_target = opts[:known_as].to_s
|
13
|
+
|
14
|
+
@peers = resolve
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(obj, reciprocal_invocation=false)
|
18
|
+
return false if peer_ids.include? obj._id
|
19
|
+
|
20
|
+
@peers << obj if @peers
|
21
|
+
peer_ids << obj._id
|
22
|
+
|
23
|
+
unless reciprocal_invocation
|
24
|
+
# Set the other side of the relationship, ensuring this method isn't called again
|
25
|
+
obj.send(@relationship_as_viewed_by_target).send(:<<, @client, true)
|
26
|
+
|
27
|
+
# Bulk save to ensure relationship is persisted on both sides
|
28
|
+
# TODO: Should this be bulk_save! ? Probably.
|
29
|
+
RelaxDB.bulk_save(@client, obj)
|
30
|
+
end
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def clear
|
36
|
+
@peers.each do |peer|
|
37
|
+
peer.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Important to resolve in the database before in memory, although an examination of the
|
41
|
+
# contents of the bulk_save will look wrong as this object will still list all its peers
|
42
|
+
RelaxDB.bulk_save(@client, *@peers)
|
43
|
+
|
44
|
+
peer_ids.clear
|
45
|
+
@peers.clear
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete(obj)
|
49
|
+
deleted = obj.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
|
50
|
+
if deleted
|
51
|
+
delete_from_self(obj)
|
52
|
+
RelaxDB.bulk_save(@client, obj)
|
53
|
+
end
|
54
|
+
deleted
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete_from_self(obj)
|
58
|
+
@peers.delete(obj)
|
59
|
+
peer_ids.delete(obj._id)
|
60
|
+
end
|
61
|
+
|
62
|
+
def empty?
|
63
|
+
peer_ids.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
def size
|
67
|
+
peer_ids.size
|
68
|
+
end
|
69
|
+
|
70
|
+
def [](*args)
|
71
|
+
@peers[*args]
|
72
|
+
end
|
73
|
+
|
74
|
+
def each(&blk)
|
75
|
+
@peers.each(&blk)
|
76
|
+
end
|
77
|
+
|
78
|
+
def inspect
|
79
|
+
@client.instance_variable_get("@#{@relationship}".to_sym).inspect
|
80
|
+
end
|
81
|
+
|
82
|
+
def peer_ids
|
83
|
+
@client.instance_variable_get("@#{@relationship}".to_sym)
|
84
|
+
end
|
85
|
+
|
86
|
+
alias to_id_a peer_ids
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Resolves the actual ids into real objects via a single GET to CouchDB
|
91
|
+
def resolve
|
92
|
+
design_doc = @client.class
|
93
|
+
view_name = @relationship
|
94
|
+
view_path = "_design/#{design_doc}/_view/#{view_name}?key=\"#{@client._id}\""
|
95
|
+
map_function = ViewCreator.has_many_through(@target_class, @relationship_as_viewed_by_target)
|
96
|
+
@peers = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|