paulcarey-relaxdb 0.2.8 → 0.3.0

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.
@@ -25,6 +25,7 @@ module RelaxDB
25
25
  obj.send(@relationship_as_viewed_by_target).send(:<<, @client, true)
26
26
 
27
27
  # Bulk save to ensure relationship is persisted on both sides
28
+ # TODO: Should this be bulk_save! ? Probably.
28
29
  RelaxDB.bulk_save(@client, obj)
29
30
  end
30
31
 
@@ -88,11 +89,8 @@ module RelaxDB
88
89
 
89
90
  # Resolves the actual ids into real objects via a single GET to CouchDB
90
91
  def resolve
91
- design_doc = @client.class
92
- view_name = @relationship
93
- view_path = "_view/#{design_doc}/#{view_name}?key=\"#{@client._id}\""
94
- map_function = ViewCreator.has_many_through(@target_class, @relationship_as_viewed_by_target)
95
- @peers = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
92
+ view_name = "#{@client.class}_#{@relationship}"
93
+ @peers = RelaxDB.view(view_name, :key => @client._id)
96
94
  end
97
95
 
98
96
  end
@@ -10,7 +10,26 @@ module RelaxDB
10
10
  class <<self
11
11
 
12
12
  def configure(config)
13
- @@db = CouchDB.new(config)
13
+ @@db = CouchDB.new config
14
+
15
+ raise "A design_doc must be provided" unless config[:design_doc]
16
+ @dd = config[:design_doc]
17
+ end
18
+
19
+ # This is a temporary method that helps the transition as RelaxDB moves to a single
20
+ # design doc per application.
21
+ def dd
22
+ @dd
23
+ end
24
+
25
+ def enable_view_creation default=true
26
+ @create_views = default
27
+ end
28
+
29
+ # Set in configuration and consulted by view_by, has_many, has_one, references_many and all
30
+ # Views will be added to CouchDB iff this is true
31
+ def create_views?
32
+ @create_views
14
33
  end
15
34
 
16
35
  def db
@@ -53,7 +72,7 @@ module RelaxDB
53
72
  resp = db.post("_bulk_docs", { "docs" => objs }.to_json )
54
73
  data = JSON.parse(resp.body)
55
74
 
56
- data["new_revs"].each do |new_rev|
75
+ data.each do |new_rev|
57
76
  obj = docs[ new_rev["id"] ]
58
77
  obj._rev = new_rev["rev"]
59
78
  obj.post_save
@@ -72,6 +91,10 @@ module RelaxDB
72
91
  false
73
92
  end
74
93
  end
94
+
95
+ def reload(obj)
96
+ load(obj._id)
97
+ end
75
98
 
76
99
  def load(ids)
77
100
  # RelaxDB.logger.debug(caller.inject("#{db.name}/#{ids}\n") { |a, i| a += "#{i}\n" })
@@ -99,32 +122,19 @@ module RelaxDB
99
122
 
100
123
  res
101
124
  end
102
-
103
- # Used internally by RelaxDB
104
- def retrieve(view_path, design_doc=nil, view_name=nil, map_func=nil, reduce_func=nil)
105
- begin
106
- resp = db.get(view_path)
107
- rescue => e
108
- dd = DesignDocument.get(design_doc).add_map_view(view_name, map_func)
109
- dd.add_reduce_view(view_name, reduce_func) if reduce_func
110
- dd.save
111
- resp = db.get(view_path)
112
- end
113
-
114
- data = JSON.parse(resp.body)
115
- ViewResult.new(data)
116
- end
117
-
118
- # Requests the given view from CouchDB and returns a hash.
119
- # This method should typically be wrapped in one of merge, instantiate, or reduce_result.
120
- def view(design_doc, view_name)
121
- q = Query.new(design_doc, view_name)
122
- yield q if block_given?
125
+
126
+ def view(view_name, params = {})
127
+ q = Query.new(view_name, params)
123
128
 
124
129
  resp = q.keys ? db.post(q.view_path, q.keys) : db.get(q.view_path)
125
- JSON.parse(resp.body)
130
+ hash = JSON.parse(resp.body)
131
+
132
+ if q.raw then hash
133
+ elsif q.reduce then reduce_result hash
134
+ else ViewResult.new hash
135
+ end
126
136
  end
127
-
137
+
128
138
  # Should be invoked on the result of a join view
129
139
  # Merges all rows based on merge_key and returns an array of ViewOject
130
140
  def merge(data, merge_key)
@@ -137,32 +147,29 @@ module RelaxDB
137
147
 
138
148
  merged.values.map { |v| ViewObject.create(v) }
139
149
  end
140
-
141
- # Creates RelaxDB::Document objects from the result
142
- def instantiate(data)
143
- create_from_hash(data)
144
- end
145
-
146
- # Returns a scalar, an object, or an Array of objects
150
+
147
151
  def reduce_result(data)
148
- obj = data["rows"][0] && data["rows"][0]["value"]
149
- ViewObject.create(obj)
152
+ res = create_from_hash data
153
+ res.size == 0 ? nil :
154
+ res.size == 1 ? res[0] : res
150
155
  end
151
156
 
152
- def paginate_view(page_params, design_doc, view_name, *view_keys)
153
- paginate_params = PaginateParams.new
154
- yield paginate_params
157
+ def paginate_view(view_name, atts)
158
+ page_params = atts.delete :page_params
159
+ view_keys = atts.delete :attributes
160
+
161
+ paginate_params = PaginateParams.new atts
155
162
  raise paginate_params.error_msg if paginate_params.invalid?
156
163
 
157
164
  paginator = Paginator.new(paginate_params, page_params)
158
165
 
159
- query = Query.new(design_doc, view_name)
166
+ query = Query.new(view_name, atts)
160
167
  query.merge(paginate_params)
161
168
 
162
169
  docs = ViewResult.new(JSON.parse(db.get(query.view_path).body))
163
170
  docs.reverse! if paginate_params.order_inverted?
164
171
 
165
- paginator.add_next_and_prev(docs, design_doc, view_name, view_keys)
172
+ paginator.add_next_and_prev(docs, view_name, view_keys)
166
173
 
167
174
  docs
168
175
  end
@@ -172,14 +179,13 @@ module RelaxDB
172
179
  end
173
180
 
174
181
  def create_object(data)
175
- # revise use of string 'class' - it's a reserved word in JavaScript
176
- klass = data.delete("class")
182
+ klass = data.is_a?(Hash) && data.delete("relaxdb_class")
177
183
  if klass
178
- k = Module.const_get(klass)
179
- k.new(data)
184
+ k = klass.split("::").inject(Object) { |x, y| x.const_get y }
185
+ k.new data
180
186
  else
181
- # data is not of a known class
182
- ViewObject.create(data)
187
+ # data is a scalar or not of a known class
188
+ ViewObject.create data
183
189
  end
184
190
  end
185
191
 
@@ -68,10 +68,10 @@ module RelaxDB
68
68
 
69
69
  # Used for test instrumentation only i.e. to assert that
70
70
  # an expected number of requests have been issued
71
- attr_accessor :get_count, :put_count
71
+ attr_accessor :get_count, :put_count, :post_count
72
72
 
73
73
  def initialize(config)
74
- @get_count, @put_count = 0, 0
74
+ @get_count, @post_count, @put_count = 0, 0, 0
75
75
  @server = RelaxDB::Server.new(config[:host], config[:port])
76
76
  @logger = config[:logger] ? config[:logger] : Logger.new(Tempfile.new('couchdb.log'))
77
77
  end
@@ -106,14 +106,14 @@ module RelaxDB
106
106
  @server.delete("/#{@db}/#{path}")
107
107
  end
108
108
 
109
- # *ignored allows methods to invoke get or post indifferently via send
110
- def get(path=nil, *ignored)
109
+ def get(path=nil)
111
110
  @get_count += 1
112
111
  @logger.info("GET /#{@db}/#{unesc(path)}")
113
112
  @server.get("/#{@db}/#{path}")
114
113
  end
115
114
 
116
115
  def post(path=nil, json=nil)
116
+ @post_count += 1
117
117
  @logger.info("POST /#{@db}/#{unesc(path)} #{json}")
118
118
  @server.post("/#{@db}/#{path}", json)
119
119
  end
@@ -6,17 +6,19 @@ module RelaxDB
6
6
 
7
7
  # Methods must start and finish on different lines
8
8
  # The function declaration must start at the beginning of a line
9
- # As '-' is used as a delimiter, neither design doc nor view name may contain '-'
9
+ # As '-' is used as a delimiter, the view name may not contain '-'
10
10
  # Exepcted function declaration form is
11
- # function DesignDoc-funcname-functype(doc) {
11
+ # function funcname-functype(doc) {
12
12
  # For example
13
- # function Users-followers-map(doc) {
13
+ # function Users_followers-map(doc) {
14
14
  #
15
15
  def upload(filename)
16
16
  lines = File.readlines(filename)
17
- extract(lines) do |dd, vn, t, f|
18
- RelaxDB::DesignDocument.get(dd).add_view(vn, t, f).save
17
+ dd = RelaxDB::DesignDocument.get(RelaxDB.dd)
18
+ extract(lines) do |vn, t, f|
19
+ dd.add_view(vn, t, f)
19
20
  end
21
+ dd.save
20
22
  end
21
23
 
22
24
  def extract(lines)
@@ -32,11 +34,11 @@ module RelaxDB
32
34
 
33
35
  0.upto(m.size-2) do |i|
34
36
  declr = lines[m[i]]
35
- declr =~ /(\w)+-(\w)+-(\w)+/
37
+ declr =~ /(\w)+-(\w)+/
36
38
  declr.sub!($&, '')
37
- design_doc, view_name, type = $&.split('-')
39
+ view_name, type = $&.split('-')
38
40
  func = lines[m[i]...m[i+1]].join
39
- yield design_doc, view_name, type, func
41
+ yield view_name, type, func
40
42
  end
41
43
  end
42
44
 
data/lib/relaxdb/views.rb CHANGED
@@ -2,51 +2,111 @@ module RelaxDB
2
2
 
3
3
  class ViewCreator
4
4
 
5
- def self.all(target_class)
5
+ def self.all(kls)
6
+ class_name = kls[0]
6
7
  map = <<-QUERY
7
- function(doc) {
8
- if(doc.class == "${target_class}")
8
+ function(doc) {
9
+ var class_match = #{kls_check kls}
10
+ if (class_match) {
9
11
  emit(null, doc);
12
+ }
10
13
  }
11
14
  QUERY
12
- map.sub!("${target_class}", target_class.to_s)
13
-
14
- reduce = <<-QUERY
15
- function(keys, values, rereduce) {
16
- if (rereduce) {
17
- return sum(values);
18
- } else {
19
- return values.length;
15
+
16
+ View.new "#{class_name}_all", map, sum_reduce_func
17
+ end
18
+
19
+ def self.by_att_list(kls, *atts)
20
+ class_name = kls[0]
21
+ key = atts.map { |a| "doc.#{a}" }.join(", ")
22
+ key = atts.size > 1 ? key.sub(/^/, "[").sub(/$/, "]") : key
23
+ prop_check = atts.map { |a| "doc.#{a} !== undefined" }.join(" && ")
24
+
25
+ map = <<-QUERY
26
+ function(doc) {
27
+ var class_match = #{kls_check kls}
28
+ if(class_match && #{prop_check}) {
29
+ emit(#{key}, doc);
20
30
  }
21
31
  }
22
32
  QUERY
23
33
 
24
- [map, reduce]
34
+ view_name = "#{class_name}_by_" << atts.join("_and_")
35
+ View.new view_name, map, sum_reduce_func
25
36
  end
37
+
26
38
 
27
- def self.has_n(target_class, relationship_to_client)
28
- template = <<-MAP_FUNC
29
- function(doc) {
30
- if(doc.class == "${target_class}" && doc.${relationship_to_client}_id)
31
- emit(doc.${relationship_to_client}_id, doc);
32
- }
33
- MAP_FUNC
34
- template.sub!("${target_class}", target_class)
35
- template.gsub("${relationship_to_client}", relationship_to_client)
39
+ def self.has_n(client_class, relationship, target_class, relationship_to_client)
40
+ map = <<-QUERY
41
+ function(doc) {
42
+ if(doc.relaxdb_class == "#{target_class}" && doc.#{relationship_to_client}_id)
43
+ emit(doc.#{relationship_to_client}_id, doc);
44
+ }
45
+ QUERY
46
+
47
+ view_name = "#{client_class}_#{relationship}"
48
+ View.new view_name, map
36
49
  end
37
50
 
38
- def self.has_many_through(target_class, peers)
39
- template = <<-MAP_FUNC
51
+ def self.references_many(client_class, relationship, target_class, peers)
52
+ map = <<-QUERY
40
53
  function(doc) {
41
- if(doc.class == "${target_class}" && doc.${peers}) {
54
+ if(doc.relaxdb_class == "#{target_class}" && doc.#{peers}) {
42
55
  var i;
43
- for(i = 0; i < doc.${peers}.length; i++) {
44
- emit(doc.${peers}[i], doc);
56
+ for(i = 0; i < doc.#{peers}.length; i++) {
57
+ emit(doc.#{peers}[i], doc);
45
58
  }
46
59
  }
47
60
  }
48
- MAP_FUNC
49
- template.sub!("${target_class}", target_class).gsub!("${peers}", peers)
61
+ QUERY
62
+
63
+ view_name = "#{client_class}_#{relationship}"
64
+ View.new view_name, map
65
+ end
66
+
67
+ def self.kls_check kls
68
+ kls_names = kls.map{ |k| %Q("#{k}") }.join(",")
69
+ "[#{kls_names}].indexOf(doc.relaxdb_class) >= 0;"
70
+ end
71
+
72
+ def self.sum_reduce_func
73
+ <<-QUERY
74
+ function(keys, values, rereduce) {
75
+ if (rereduce) {
76
+ return sum(values);
77
+ } else {
78
+ return values.length;
79
+ }
80
+ }
81
+ QUERY
82
+ end
83
+
84
+ end
85
+
86
+ class View
87
+
88
+ attr_reader :view_name
89
+
90
+ def initialize view_name, map_func, reduce_func = nil
91
+ @view_name = view_name
92
+ @map_func = map_func
93
+ @reduce_func = reduce_func
94
+ end
95
+
96
+ def design_doc
97
+ @design_doc ||= DesignDocument.get(RelaxDB.dd)
98
+ end
99
+
100
+ def save
101
+ dd = design_doc
102
+ dd.add_map_view(@view_name, @map_func)
103
+ dd.add_reduce_view(@view_name, @reduce_func) if @reduce_func
104
+ dd.save
105
+ end
106
+
107
+ def exists?
108
+ dd = design_doc
109
+ dd.data["views"] && dd.data["views"][@view_name]
50
110
  end
51
111
 
52
112
  end
data/lib/relaxdb.rb CHANGED
@@ -28,7 +28,6 @@ require 'relaxdb/query'
28
28
  require 'relaxdb/references_many_proxy'
29
29
  require 'relaxdb/relaxdb'
30
30
  require 'relaxdb/server'
31
- require 'relaxdb/sorted_by_view'
32
31
  require 'relaxdb/uuid_generator'
33
32
  require 'relaxdb/view_object'
34
33
  require 'relaxdb/view_result'
data/readme.rb ADDED
@@ -0,0 +1,79 @@
1
+ # README code is extracted from this file (pagination code excepted).
2
+
3
+ require 'rubygems'
4
+ require 'lib/relaxdb'
5
+
6
+ RelaxDB.configure :host => "localhost", :port => 5984, :design_doc => "app" #, :logger => Logger.new(STDOUT)
7
+ RelaxDB.delete_db "relaxdb_scratch" rescue :ok
8
+ RelaxDB.use_db "relaxdb_scratch"
9
+ RelaxDB.enable_view_creation # creates views when class definition is executed
10
+
11
+ class User < RelaxDB::Document
12
+ property :name
13
+ end
14
+
15
+ class Invite < RelaxDB::Document
16
+
17
+ property :created_at
18
+
19
+ property :event_name
20
+
21
+ property :state, :default => "awaiting_response",
22
+ :validator => lambda { |s| %w(accepted rejected awaiting_response).include? s }
23
+
24
+ references :sender, :validator => :required
25
+
26
+ references :recipient, :validator => :required
27
+
28
+ property :sender_name,
29
+ :derived => [:sender, lambda { |p, o| o.sender.name } ]
30
+
31
+ view_by :sender_name
32
+ view_by :sender_id
33
+ view_by :recipient_id, :created_at, :descending => true
34
+
35
+ def on_update_conflict
36
+ puts "conflict!"
37
+ end
38
+
39
+ end
40
+
41
+ # Saving objects
42
+
43
+ sofa = User.new(:name => "sofa").save!
44
+ futon = User.new(:name => "futon").save!
45
+
46
+ i = Invite.new :sender => sofa, :recipient => futon, :event_name => "CouchCamp"
47
+ i.save!
48
+
49
+ # Loading and querying
50
+
51
+ il = RelaxDB.load i._id
52
+ puts i == il # true
53
+
54
+ ir = Invite.by_sender_name "sofa"
55
+ puts i == ir # true
56
+
57
+ ix = Invite.by_sender_name(:key => "sofa").first
58
+ puts i == ix # true
59
+
60
+ # Denormalization
61
+
62
+ puts ix.sender_name # prints sofa, no requests to CouchDB made
63
+ puts ix.sender.name # prints sofa, a single CouchDB request made
64
+
65
+ # Saving with conflicts
66
+
67
+ idup = i.dup
68
+ i.save!
69
+ idup.save # conflict printed
70
+
71
+ # Saving with and without validations
72
+
73
+ i = Invite.new :sender => sofa, :name => "daily show"
74
+ i.save! rescue :ok # save! throws an exception on validation failure or conflict
75
+ i.save # returns false rather than throwing an exception
76
+ puts i.errors.inspect # {:recipient=>"invalid:"}
77
+
78
+ i.validation_skip_list << :recipient # Any and all validations may be skipped
79
+ i.save # succeeds
@@ -4,12 +4,7 @@ require File.dirname(__FILE__) + '/spec_models.rb'
4
4
  describe RelaxDB::BelongsToProxy do
5
5
 
6
6
  before(:all) do
7
- RelaxDB.configure(:host => "localhost", :port => 5984)
8
- end
9
-
10
- before(:each) do
11
- RelaxDB.delete_db "relaxdb_spec_db" rescue "ok"
12
- RelaxDB.use_db "relaxdb_spec_db"
7
+ setup_test_db
13
8
  end
14
9
 
15
10
  describe "belongs_to" do
@@ -5,12 +5,12 @@ require File.dirname(__FILE__) + '/spec_helper.rb'
5
5
  describe RelaxDB::Document, "callbacks" do
6
6
 
7
7
  before(:all) do
8
- RelaxDB.configure(:host => "localhost", :port => 5984)
8
+ RelaxDB.configure :host => "localhost", :port => 5984, :design_doc => "spec_doc"
9
9
  end
10
10
 
11
11
  before(:each) do
12
- RelaxDB.delete_db "relaxdb_spec_db" rescue "ok"
13
- RelaxDB.use_db "relaxdb_spec_db"
12
+ RelaxDB.delete_db "relaxdb_spec" rescue "ok"
13
+ RelaxDB.use_db "relaxdb_spec"
14
14
  end
15
15
 
16
16
  describe "before_save" do
@@ -31,6 +31,22 @@ describe RelaxDB::Document, "callbacks" do
31
31
  c.new.save.should == false
32
32
  end
33
33
 
34
+ it "should add a description to errors when false is returned" do
35
+ c = Class.new(RelaxDB::Document) do
36
+ before_save lambda { false }
37
+ end
38
+ x = c.new
39
+ x.save
40
+ x.errors[:before_save].should be
41
+ end
42
+
43
+ it "should not prevent the object from being saved if it returns nil" do
44
+ c = Class.new(RelaxDB::Document) do
45
+ before_save lambda { nil }
46
+ end
47
+ c.new.save!
48
+ end
49
+
34
50
  it "may be a proc" do
35
51
  c = Class.new(RelaxDB::Document) do
36
52
  before_save lambda { false }
@@ -12,13 +12,8 @@ end
12
12
 
13
13
  describe RelaxDB::Document, "derived properties" do
14
14
 
15
- before(:all) do
16
- RelaxDB.configure(:host => "localhost", :port => 5984)
17
- end
18
-
19
15
  before(:each) do
20
- RelaxDB.delete_db "relaxdb_spec_db" rescue "ok"
21
- RelaxDB.use_db "relaxdb_spec_db"
16
+ setup_test_db
22
17
  end
23
18
 
24
19
  it "should have its value updated when the source is updated" do
@@ -36,7 +31,7 @@ describe RelaxDB::Document, "derived properties" do
36
31
  i.event_name.should == "shindig"
37
32
  RelaxDB.db.get_count.should == 1
38
33
  end
39
-
34
+
40
35
  it "should have its value updated when the source_id is updated for a saved event" do
41
36
  e = DpEvent.new(:name => "shindig").save!
42
37
  i = DpInvite.new(:event_id => e._id)
@@ -4,12 +4,12 @@ require File.dirname(__FILE__) + '/spec_models.rb'
4
4
  describe RelaxDB::DesignDocument do
5
5
 
6
6
  before(:all) do
7
- RelaxDB.configure(:host => "localhost", :port => 5984)
7
+ RelaxDB.configure :host => "localhost", :port => 5984, :design_doc => "spec_doc"
8
8
  end
9
9
 
10
10
  before(:each) do
11
- RelaxDB.delete_db "relaxdb_spec_db" rescue "ok"
12
- RelaxDB.use_db "relaxdb_spec_db"
11
+ RelaxDB.delete_db "relaxdb_spec" rescue "ok"
12
+ RelaxDB.use_db "relaxdb_spec"
13
13
  end
14
14
 
15
15
  describe "#save" do