cohitre-relaxdb 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ class Errors < Hash
2
+ alias_method :on, :[]
3
+ end
@@ -0,0 +1,81 @@
1
+ module RelaxDB
2
+
3
+ class HasManyProxy
4
+
5
+ include Enumerable
6
+
7
+ def initialize(client, relationship, opts)
8
+ @client = client
9
+ @relationship = relationship
10
+ @opts = opts
11
+
12
+ @target_class = opts[:class]
13
+ @relationship_as_viewed_by_target = (opts[:known_as] || client.class.name.snake_case).to_s
14
+
15
+ @children = load_children
16
+ end
17
+
18
+ def <<(obj)
19
+ return false unless obj.validates?
20
+ return false if @children.include?(obj)
21
+
22
+ obj.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
23
+ obj.save
24
+ @children << obj
25
+ self
26
+ end
27
+
28
+ def clear
29
+ @children.each do |c|
30
+ break_back_link c
31
+ end
32
+ @children.clear
33
+ end
34
+
35
+ def delete(obj)
36
+ obj = @children.delete(obj)
37
+ break_back_link(obj) if obj
38
+ end
39
+
40
+ def break_back_link(obj)
41
+ if obj
42
+ obj.send("#{@relationship_as_viewed_by_target}=".to_sym, nil)
43
+ obj.save
44
+ end
45
+ end
46
+
47
+ def empty?
48
+ @children.empty?
49
+ end
50
+
51
+ def size
52
+ @children.size
53
+ end
54
+
55
+ def [](*args)
56
+ @children[*args]
57
+ end
58
+
59
+ def each(&blk)
60
+ @children.each(&blk)
61
+ end
62
+
63
+ def reload
64
+ @children = load_children
65
+ end
66
+
67
+ def load_children
68
+ view_path = "_view/#{@client.class}/#{@relationship}?key=\"#{@client._id}\""
69
+ design_doc = @client.class
70
+ view_name = @relationship
71
+ map_function = ViewCreator.has_n(@target_class, @relationship_as_viewed_by_target)
72
+ @children = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
73
+ end
74
+
75
+ def inspect
76
+ @children.inspect
77
+ end
78
+
79
+ end
80
+
81
+ 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 = "_view/#{design_doc}/#{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 count 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.group(true).group_level(0).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).count(1)
73
+ RelaxDB.retrieve(query.view_path).offset
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,74 @@
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").count(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 count 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
+ end
46
+ end
47
+
48
+ def view_path
49
+ uri = "_view/#{@design_doc}/#{@view_name}"
50
+
51
+ query = ""
52
+ @@params.each do |param|
53
+ val_set = instance_variable_get("@#{param}_set")
54
+ if val_set
55
+ val = instance_variable_get("@#{param}")
56
+ val = val.to_json unless ["startkey_docid", "endkey_docid"].include?(param)
57
+ query << "&#{param}=#{::CGI::escape(val)}"
58
+ end
59
+ end
60
+
61
+ uri << query.sub(/^&/, "?")
62
+ end
63
+
64
+ def merge(paginate_params)
65
+ paginate_params.instance_variables.each do |pp|
66
+ val = paginate_params.instance_variable_get(pp)
67
+ method_name = pp[1, pp.length]
68
+ send(method_name, val) if methods.include? method_name
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,99 @@
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
+ end
14
+
15
+ def <<(obj, reciprocal_invocation=false)
16
+ return false if peer_ids.include? obj._id
17
+
18
+ @peers << obj if @peers
19
+ peer_ids << obj._id
20
+
21
+ unless reciprocal_invocation
22
+ # Set the other side of the relationship, ensuring this method isn't called again
23
+ obj.send(@relationship_as_viewed_by_target).send(:<<, @client, true)
24
+
25
+ # Bulk save to ensure relationship is persisted on both sides
26
+ RelaxDB.bulk_save(@client, obj)
27
+ end
28
+
29
+ self
30
+ end
31
+
32
+ def clear
33
+ resolve
34
+ @peers.each do |peer|
35
+ peer.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
36
+ end
37
+
38
+ # Important to resolve in the database before in memory, although an examination of the
39
+ # contents of the bulk_save will look wrong as this object will still list all its peers
40
+ RelaxDB.bulk_save(@client, *@peers)
41
+
42
+ peer_ids.clear
43
+ @peers.clear
44
+ end
45
+
46
+ def delete(obj)
47
+ deleted = obj.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
48
+ if deleted
49
+ delete_from_self(obj)
50
+ RelaxDB.bulk_save(@client, obj)
51
+ end
52
+ deleted
53
+ end
54
+
55
+ def delete_from_self(obj)
56
+ @peers.delete(obj) if @peers
57
+ peer_ids.delete(obj._id)
58
+ end
59
+
60
+ def empty?
61
+ peer_ids.empty?
62
+ end
63
+
64
+ def size
65
+ peer_ids.size
66
+ end
67
+
68
+ def [](*args)
69
+ resolve
70
+ @peers[*args]
71
+ end
72
+
73
+ def each(&blk)
74
+ resolve
75
+ @peers.each(&blk)
76
+ end
77
+
78
+ def inspect
79
+ @client.instance_variable_get("@#{@relationship}".to_sym).inspect
80
+ end
81
+
82
+ private
83
+
84
+ def peer_ids
85
+ @client.instance_variable_get("@#{@relationship}".to_sym)
86
+ end
87
+
88
+ # Resolves the actual ids into real objects via a single GET to CouchDB. Called internally by each
89
+ def resolve
90
+ design_doc = @client.class
91
+ view_name = @relationship
92
+ view_path = "_view/#{design_doc}/#{view_name}?key=\"#{@client._id}\""
93
+ map_function = ViewCreator.has_many_through(@target_class, @relationship_as_viewed_by_target)
94
+ @peers = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
95
+ end
96
+
97
+ end
98
+
99
+ end
@@ -0,0 +1,157 @@
1
+ module RelaxDB
2
+
3
+ @@db = nil
4
+
5
+ class <<self
6
+
7
+ def configure(config)
8
+ @@db = CouchDB.new(config)
9
+ end
10
+
11
+ def db
12
+ @@db
13
+ end
14
+
15
+ def logger
16
+ @@db.logger
17
+ end
18
+
19
+ # Creates the named database if it doesn't already exist
20
+ def use_db(name)
21
+ db.use_db(name)
22
+ end
23
+
24
+ def delete_db(name)
25
+ db.delete_db(name)
26
+ end
27
+
28
+ def list_dbs
29
+ db.list_dbs
30
+ end
31
+
32
+ def replicate_db(source, target)
33
+ db.replicate_db source, target
34
+ end
35
+
36
+ def bulk_save(*objs)
37
+ docs = {}
38
+ objs.each { |o| docs[o._id] = o }
39
+
40
+ resp = db.post("_bulk_docs", { "docs" => objs }.to_json )
41
+ data = JSON.parse(resp.body)
42
+
43
+ data["new_revs"].each do |new_rev|
44
+ docs[ new_rev["id"] ]._rev = new_rev["rev"]
45
+ end
46
+
47
+ data["ok"]
48
+ end
49
+
50
+ def load(*ids)
51
+ if ids.size == 1
52
+ resp = db.get(ids[0])
53
+ data = JSON.parse(resp.body)
54
+ create_object(data)
55
+ else
56
+ resp = db.post("_all_docs?include_docs=true", {:keys => ids}.to_json)
57
+ data = JSON.parse(resp.body)
58
+ data["rows"].map { |row| create_object(row["doc"]) }
59
+ end
60
+ end
61
+
62
+ # Used internally by RelaxDB
63
+ def retrieve(view_path, design_doc=nil, view_name=nil, map_function=nil)
64
+ begin
65
+ resp = db.get(view_path)
66
+ rescue => e
67
+ DesignDocument.get(design_doc).add_map_view(view_name, map_function).save
68
+ resp = db.get(view_path)
69
+ end
70
+
71
+ data = JSON.parse(resp.body)
72
+ ViewResult.new(data)
73
+ end
74
+
75
+ # Requests the given view from CouchDB and returns a hash.
76
+ # This method should typically be wrapped in one of merge, instantiate, or reduce_result.
77
+ def view(design_doc, view_name)
78
+ q = Query.new(design_doc, view_name)
79
+ yield q if block_given?
80
+
81
+ resp = q.keys ? db.post(q.view_path, q.keys) : db.get(q.view_path)
82
+ JSON.parse(resp.body)
83
+ end
84
+
85
+ # Should be invoked on the result of a join view
86
+ # Merges all rows based on merge_key and returns an array of ViewOject
87
+ def merge(data, merge_key)
88
+ merged = {}
89
+ data["rows"].each do |row|
90
+ value = row["value"]
91
+ merged[value[merge_key]] ||= {}
92
+ merged[value[merge_key]].merge!(value)
93
+ end
94
+
95
+ merged.values.map { |v| ViewObject.create(v) }
96
+ end
97
+
98
+ # Creates RelaxDB::Document objects from the result
99
+ def instantiate(data)
100
+ create_from_hash(data)
101
+ end
102
+
103
+ # Returns a scalar, an object, or an Array of objects
104
+ def reduce_result(data)
105
+ obj = data["rows"][0] && data["rows"][0]["value"]
106
+ ViewObject.create(obj)
107
+ end
108
+
109
+ def paginate_view(page_params, design_doc, view_name, *view_keys)
110
+ paginate_params = PaginateParams.new
111
+ yield paginate_params
112
+ raise paginate_params.error_msg if paginate_params.invalid?
113
+
114
+ paginator = Paginator.new(paginate_params, page_params)
115
+
116
+ query = Query.new(design_doc, view_name)
117
+ query.merge(paginate_params)
118
+
119
+ docs = ViewResult.new(JSON.parse(db.get(query.view_path).body))
120
+ docs.reverse! if paginate_params.order_inverted?
121
+
122
+ paginator.add_next_and_prev(docs, design_doc, view_name, view_keys)
123
+
124
+ docs
125
+ end
126
+
127
+ def create_from_hash(data)
128
+ data["rows"].map { |row| create_object(row["value"]) }
129
+ end
130
+
131
+ def create_object(data)
132
+ # revise use of string 'class' - it's a reserved word in JavaScript
133
+ klass = data.delete("class")
134
+ if klass
135
+ k = Module.const_get(klass)
136
+ k.new(data)
137
+ else
138
+ # data is not of a known class
139
+ ViewObject.create(data)
140
+ end
141
+ end
142
+
143
+ # Convenience methods - should be in a diffent module?
144
+
145
+ def pp_get(uri=nil)
146
+ resp = db.get(uri)
147
+ pp(JSON.parse(resp.body))
148
+ end
149
+
150
+ def pp_post(uri=nil, json=nil)
151
+ resp = db.post(uri, json)
152
+ pp(JSON.parse(resp.body))
153
+ end
154
+
155
+ end
156
+
157
+ end