ceritium-relaxdb 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/LICENSE +20 -0
  2. data/README.textile +185 -0
  3. data/Rakefile +58 -0
  4. data/docs/spec_results.html +604 -0
  5. data/lib/more/atomic_bulk_save_support.rb +18 -0
  6. data/lib/more/grapher.rb +48 -0
  7. data/lib/relaxdb.rb +40 -0
  8. data/lib/relaxdb/all_delegator.rb +68 -0
  9. data/lib/relaxdb/belongs_to_proxy.rb +29 -0
  10. data/lib/relaxdb/design_doc.rb +55 -0
  11. data/lib/relaxdb/document.rb +531 -0
  12. data/lib/relaxdb/extlib.rb +15 -0
  13. data/lib/relaxdb/has_many_proxy.rb +104 -0
  14. data/lib/relaxdb/has_one_proxy.rb +45 -0
  15. data/lib/relaxdb/paginate_params.rb +54 -0
  16. data/lib/relaxdb/paginator.rb +78 -0
  17. data/lib/relaxdb/query.rb +75 -0
  18. data/lib/relaxdb/references_many_proxy.rb +101 -0
  19. data/lib/relaxdb/relaxdb.rb +208 -0
  20. data/lib/relaxdb/server.rb +156 -0
  21. data/lib/relaxdb/sorted_by_view.rb +65 -0
  22. data/lib/relaxdb/uuid_generator.rb +21 -0
  23. data/lib/relaxdb/validators.rb +11 -0
  24. data/lib/relaxdb/view_object.rb +34 -0
  25. data/lib/relaxdb/view_result.rb +18 -0
  26. data/lib/relaxdb/view_uploader.rb +47 -0
  27. data/lib/relaxdb/views.rb +54 -0
  28. data/spec/belongs_to_spec.rb +129 -0
  29. data/spec/callbacks_spec.rb +80 -0
  30. data/spec/derived_properties_spec.rb +117 -0
  31. data/spec/design_doc_spec.rb +34 -0
  32. data/spec/document_spec.rb +556 -0
  33. data/spec/has_many_spec.rb +176 -0
  34. data/spec/has_one_spec.rb +128 -0
  35. data/spec/query_spec.rb +80 -0
  36. data/spec/references_many_spec.rb +178 -0
  37. data/spec/relaxdb_spec.rb +226 -0
  38. data/spec/spec.opts +1 -0
  39. data/spec/spec_helper.rb +10 -0
  40. data/spec/spec_models.rb +151 -0
  41. data/spec/view_object_spec.rb +47 -0
  42. 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