cohitre-relaxdb 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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