couchobject 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +10 -0
- data/Manifest.txt +30 -6
- data/README.txt +580 -42
- data/TODO +2 -2
- data/config/hoe.rb +1 -1
- data/lib/couch_object.rb +7 -2
- data/lib/couch_object/database.rb +19 -34
- data/lib/couch_object/document.rb +13 -6
- data/lib/couch_object/error_classes.rb +110 -0
- data/lib/couch_object/persistable.rb +954 -36
- data/lib/couch_object/persistable/has_many_relations_array.rb +91 -0
- data/lib/couch_object/persistable/meta_classes.rb +568 -0
- data/lib/couch_object/persistable/overloaded_methods.rb +209 -0
- data/lib/couch_object/server.rb +1 -1
- data/lib/couch_object/utils.rb +44 -0
- data/lib/couch_object/version.rb +1 -1
- data/lib/couch_object/view.rb +129 -6
- data/script/console +0 -0
- data/script/destroy +0 -0
- data/script/generate +0 -0
- data/script/txt2html +0 -0
- data/spec/database_spec.rb +23 -31
- data/spec/database_spec.rb.orig +173 -0
- data/spec/document_spec.rb +21 -3
- data/spec/integration/database_integration_spec.rb +46 -15
- data/spec/integration/integration_helper.rb +3 -3
- data/spec/persistable/callback.rb +44 -0
- data/spec/persistable/callback_spec.rb +44 -0
- data/spec/persistable/cloning.rb +77 -0
- data/spec/persistable/cloning_spec.rb +77 -0
- data/spec/persistable/comparing_objects.rb +350 -0
- data/spec/persistable/comparing_objects_spec.rb +350 -0
- data/spec/persistable/deleting.rb +113 -0
- data/spec/persistable/deleting_spec.rb +113 -0
- data/spec/persistable/error_messages.rb +32 -0
- data/spec/persistable/error_messages_spec.rb +32 -0
- data/spec/persistable/loading.rb +339 -0
- data/spec/persistable/loading_spec.rb +339 -0
- data/spec/persistable/new_methods.rb +70 -0
- data/spec/persistable/new_methods_spec.rb +70 -0
- data/spec/persistable/persistable_helper.rb +194 -0
- data/spec/persistable/relations.rb +470 -0
- data/spec/persistable/relations_spec.rb +470 -0
- data/spec/persistable/saving.rb +137 -0
- data/spec/persistable/saving_spec.rb +137 -0
- data/spec/persistable/setting_storage_location.rb +65 -0
- data/spec/persistable/setting_storage_location_spec.rb +65 -0
- data/spec/persistable/timestamps.rb +76 -0
- data/spec/persistable/timestamps_spec.rb +76 -0
- data/spec/persistable/unsaved_changes.rb +211 -0
- data/spec/persistable/unsaved_changes_spec.rb +211 -0
- data/spec/server_spec.rb +5 -5
- data/spec/utils_spec.rb +60 -0
- data/spec/view_spec.rb +40 -7
- data/website/index.html +22 -7
- data/website/index.txt +13 -5
- metadata +93 -61
- data/bin/couch_ruby_view_requestor +0 -81
- data/lib/couch_object/model.rb +0 -5
- data/lib/couch_object/proc_condition.rb +0 -14
- data/spec/model_spec.rb +0 -5
- data/spec/persistable_spec.rb +0 -91
- data/spec/proc_condition_spec.rb +0 -26
data/TODO
CHANGED
data/config/hoe.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'couch_object/version'
|
2
2
|
|
3
|
-
AUTHOR = 'Johan Sørensen' # can also be an array of Authors
|
3
|
+
AUTHOR = ['Johan Sørensen','Sebastian Probst Eide'] # can also be an array of Authors
|
4
4
|
EMAIL = "johan@johansorensen.com"
|
5
5
|
DESCRIPTION = "CouchObject is a library that maps ruby objects to CouchDb documents"
|
6
6
|
GEM_NAME = 'couchobject' # what ppl will type to install your gem
|
data/lib/couch_object.rb
CHANGED
@@ -11,12 +11,16 @@ rescue LoadError
|
|
11
11
|
end
|
12
12
|
|
13
13
|
require 'json/add/core'
|
14
|
-
require
|
14
|
+
require 'thread'
|
15
|
+
|
16
|
+
# require "htmlentities" # if you want the UTILS::decode_strings to
|
17
|
+
# replace characters like ø with ø
|
18
|
+
# then this has to be included...
|
19
|
+
require "kconv" # To convert all input to UTF-8 before it is made to_json
|
15
20
|
|
16
21
|
$:.unshift File.dirname(__FILE__)
|
17
22
|
|
18
23
|
require "couch_object/utils"
|
19
|
-
require "couch_object/proc_condition"
|
20
24
|
require "couch_object/document"
|
21
25
|
require "couch_object/response"
|
22
26
|
require "couch_object/server"
|
@@ -24,6 +28,7 @@ require "couch_object/database"
|
|
24
28
|
require "couch_object/view"
|
25
29
|
require "couch_object/persistable"
|
26
30
|
require "couch_object/model"
|
31
|
+
require "couch_object/error_classes"
|
27
32
|
|
28
33
|
module CouchObject
|
29
34
|
|
@@ -2,7 +2,7 @@ module CouchObject
|
|
2
2
|
# A CouchDb database object
|
3
3
|
class Database
|
4
4
|
# Create a new database at +uri+ with the name if +dbname+
|
5
|
-
def self.create
|
5
|
+
def self.create(uri, dbname)
|
6
6
|
server = Server.new(uri)
|
7
7
|
response = Response.new(server.put("/#{dbname}", "")).parse
|
8
8
|
response.parsed_body
|
@@ -25,7 +25,7 @@ module CouchObject
|
|
25
25
|
end
|
26
26
|
|
27
27
|
# Open a connection to the database at +uri+, where +uri+ is a full uri
|
28
|
-
# like: http://localhost:
|
28
|
+
# like: http://localhost:5984/foo
|
29
29
|
def self.open(uri)
|
30
30
|
uri = URI.parse(uri)
|
31
31
|
server_uri = "#{uri.scheme}://#{uri.host}:#{uri.port}"
|
@@ -39,7 +39,7 @@ module CouchObject
|
|
39
39
|
end
|
40
40
|
attr_accessor :server
|
41
41
|
|
42
|
-
# The full url of this database, eg http://localhost:
|
42
|
+
# The full url of this database, eg http://localhost:5984/foo
|
43
43
|
def url
|
44
44
|
Utils.join_url(@uri, @dbname).to_s
|
45
45
|
end
|
@@ -50,35 +50,37 @@ module CouchObject
|
|
50
50
|
end
|
51
51
|
|
52
52
|
# Send a GET request to the +path+ which is relative to the database path
|
53
|
-
# so calling with with "bar" as the path in the "foo_db" database will
|
54
|
-
# http://host:port/foo_db/bar.
|
53
|
+
# so calling with with "bar" as the path in the "foo_db" database will
|
54
|
+
# call http://host:port/foo_db/bar.
|
55
55
|
# Returns a Response object
|
56
56
|
def get(path)
|
57
57
|
Response.new(@server.get("/#{Utils.join_url(@dbname, path)}")).parse
|
58
58
|
end
|
59
59
|
|
60
60
|
# Send a POST request to the +path+ which is relative to the database path
|
61
|
-
# so calling with with "bar" as the path in the "foo_db" database will
|
62
|
-
# http://host:port/foo_db/bar. The post body is the +payload+
|
61
|
+
# so calling with with "bar" as the path in the "foo_db" database will
|
62
|
+
# call http://host:port/foo_db/bar. The post body is the +payload+
|
63
63
|
# Returns a Response object
|
64
|
-
def post(path, payload)
|
65
|
-
Response.new(@server.post("/#{Utils.join_url(@dbname, path)}", payload)).parse
|
64
|
+
def post(path, payload, content_type="application/json")
|
65
|
+
Response.new(@server.post("/#{Utils.join_url(@dbname, path)}", payload, content_type)).parse
|
66
66
|
end
|
67
67
|
|
68
68
|
# Send a PUT request to the +path+ which is relative to the database path
|
69
|
-
# so calling with with "bar" as the path in the "foo_db" database will
|
70
|
-
# http://host:port/foo_db/bar. The put body is the +payload+
|
69
|
+
# so calling with with "bar" as the path in the "foo_db" database will
|
70
|
+
# call http://host:port/foo_db/bar. The put body is the +payload+
|
71
71
|
# Returns a Response object
|
72
72
|
def put(path, payload="")
|
73
|
-
Response.new(@server.
|
73
|
+
Response.new(@server.
|
74
|
+
put("/#{Utils.join_url(@dbname, path)}", payload)).parse
|
74
75
|
end
|
75
76
|
|
76
|
-
# Send a DELETE request to the +path+ which is relative to the
|
77
|
-
# so calling with with "bar" as the path in the "foo_db"
|
78
|
-
# http://host:port/foo_db/bar.
|
77
|
+
# Send a DELETE request to the +path+ which is relative to the
|
78
|
+
# database path so calling with with "bar" as the path in the "foo_db"
|
79
|
+
# database will call http://host:port/foo_db/bar.
|
79
80
|
# Returns a Response object
|
80
|
-
def delete(path)
|
81
|
-
Response.new(@server.
|
81
|
+
def delete(path, revision)
|
82
|
+
Response.new(@server.
|
83
|
+
delete("/#{Utils.join_url(@dbname, path)}?rev=#{revision}")).parse
|
82
84
|
end
|
83
85
|
|
84
86
|
# Get a document by id
|
@@ -105,23 +107,6 @@ module CouchObject
|
|
105
107
|
document.save(self)
|
106
108
|
end
|
107
109
|
|
108
|
-
# Queries the database with the block (using a temp. view)
|
109
|
-
# Requires a block argument that's the doc thats evaluted in
|
110
|
-
# CouchDb
|
111
|
-
#
|
112
|
-
# >> pp db.filter do |doc|
|
113
|
-
# if doc["foo"] == "baz"
|
114
|
-
# return doc["foo"]
|
115
|
-
# end
|
116
|
-
# end
|
117
|
-
# [{"_rev"=>928806717,
|
118
|
-
# "_id"=>"28D568C5992CBD2B4711F57225A19517",
|
119
|
-
# "value"=>"baz"}]
|
120
|
-
def filter(&block)
|
121
|
-
resp = Response.new(post("_temp_view", ProcCondition.new(&block).to_ruby)).parse
|
122
|
-
resp.to_document.rows
|
123
|
-
end
|
124
|
-
|
125
110
|
def views(view_name)
|
126
111
|
view = View.new(self, view_name)
|
127
112
|
view.query
|
@@ -7,8 +7,8 @@ module CouchObject
|
|
7
7
|
# the document values
|
8
8
|
def initialize(attributes={})
|
9
9
|
@attributes = attributes.dup
|
10
|
-
@id = @attributes.delete("_id")
|
11
|
-
@revision = @attributes.delete("_rev")
|
10
|
+
@id = @attributes.delete("_id") || @attributes.delete("id")
|
11
|
+
@revision = @attributes.delete("_rev") || @attributes.delete("rev")
|
12
12
|
end
|
13
13
|
attr_accessor :attributes, :id, :revision
|
14
14
|
|
@@ -79,14 +79,14 @@ module CouchObject
|
|
79
79
|
def create(database)
|
80
80
|
response = database.post("", self.to_json)
|
81
81
|
# TODO error handling
|
82
|
-
|
83
|
-
@revision = response.to_document.revision
|
84
|
-
response
|
82
|
+
apply_response(response)
|
85
83
|
end
|
86
84
|
|
87
85
|
def update(database)
|
88
|
-
|
86
|
+
revision ? json = self.to_json("_rev" => revision) : json = self.to_json
|
87
|
+
response = database.put(id, json)
|
89
88
|
# TODO error handling
|
89
|
+
apply_response(response)
|
90
90
|
end
|
91
91
|
|
92
92
|
private
|
@@ -102,5 +102,12 @@ module CouchObject
|
|
102
102
|
has_key?(method_name) ? self[method_name] : super
|
103
103
|
end
|
104
104
|
end
|
105
|
+
|
106
|
+
def apply_response(response)
|
107
|
+
doc = response.to_document
|
108
|
+
@id = doc.id
|
109
|
+
@revision = doc.revision
|
110
|
+
response
|
111
|
+
end
|
105
112
|
end
|
106
113
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module CouchObject
|
2
|
+
module Errors
|
3
|
+
#
|
4
|
+
# Is raised when trying to load a view that doesn't exist on
|
5
|
+
# the couchDB server
|
6
|
+
#
|
7
|
+
class MissingView < StandardError
|
8
|
+
def message
|
9
|
+
"The view doesn't exist on the server."
|
10
|
+
end
|
11
|
+
alias_method :to_s, :message
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
#####
|
16
|
+
#
|
17
|
+
# ERRORS for the Persistable module
|
18
|
+
#
|
19
|
+
#####
|
20
|
+
|
21
|
+
#
|
22
|
+
# Is raised when a method that interacts with the
|
23
|
+
# document store is called and the location variable
|
24
|
+
# has not been set
|
25
|
+
#
|
26
|
+
class NoDatabaseLocationSet < StandardError
|
27
|
+
def message
|
28
|
+
"Unless the document has previously been saved you need to " + \
|
29
|
+
"supply the full URI to the CouchDB server where the document " + \
|
30
|
+
"is to be saved."
|
31
|
+
end
|
32
|
+
alias_method :to_s, :message
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Is raised when trying to compare two object of which
|
37
|
+
# none has been saved.
|
38
|
+
#
|
39
|
+
class CantCompareSize < StandardError
|
40
|
+
def message
|
41
|
+
"Can't compare the size of two objects unless at least one of " + \
|
42
|
+
"them has been saved and both have timestamps."
|
43
|
+
end
|
44
|
+
alias_method :to_s, :message
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Is raised if a has_many relationship is poorly defined
|
49
|
+
#
|
50
|
+
class HasManyAssociationError < StandardError
|
51
|
+
def message
|
52
|
+
"The has_many relationship hasn't been correctly defined. It should look something like: has_many :fruits. Make sure to also include a belongs_to relationship in the related classes that reference back to the has_many relationship. For example like this: belongs_to :fruit_basket, :as => :fruits"
|
53
|
+
end
|
54
|
+
alias_method :to_s, :message
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Is raised if a belongs_to relationship is poorly defined
|
59
|
+
#
|
60
|
+
class BelongsToAssociationError < StandardError
|
61
|
+
def message
|
62
|
+
"The belongs_to relationship hasn't been properly defined. There are two required parameters: the name of the relation and the name of the corresponding has_many relation in the related class. Example: belongs_to :fruit_basket, :as => :fruits would be correct if the related class has it's relation defined as has_many :fruits"
|
63
|
+
end
|
64
|
+
alias_method :to_s, :message
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Is raised if a has_one relationship is poorly defined
|
69
|
+
#
|
70
|
+
class HasOneAssociationError < StandardError
|
71
|
+
def message
|
72
|
+
"The has_one relationship hasn't been correctly defined. It should look something like: has_one :sword. Make sure to also include a belongs_to relationship in the related classes that reference back to the has_one relationship. For example like this: belongs_to :master, :as => :sword"
|
73
|
+
end
|
74
|
+
alias_method :to_s, :message
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# This error is raised when trying to load a document that doesn't
|
79
|
+
# exist on the server
|
80
|
+
#
|
81
|
+
class DocumentNotFound < StandardError
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# This error is raised when trying to save or updating
|
86
|
+
# a document fails
|
87
|
+
#
|
88
|
+
class DatabaseSaveFailed < StandardError
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# This error is raised when trying to load a view
|
93
|
+
# but CouchDB screwes it up and returns a MapProcessError
|
94
|
+
#
|
95
|
+
class MapProcessError < StandardError
|
96
|
+
def message
|
97
|
+
"CouchDB screwed it up somehow. There might be something wrong with your view as well."
|
98
|
+
end
|
99
|
+
alias_method :to_s, :message
|
100
|
+
end
|
101
|
+
|
102
|
+
#
|
103
|
+
# Is raised when one the CouchDB returns an error which isn't covered
|
104
|
+
# by one of the functions above
|
105
|
+
#
|
106
|
+
class CouchDBError < StandardError
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -1,59 +1,977 @@
|
|
1
|
+
# = Persistable
|
2
|
+
#
|
3
|
+
# == Introduction
|
4
|
+
#
|
5
|
+
# The persistable mixin let's you save and load your classes
|
6
|
+
# to and from CoachDB using the instance method +save+ and
|
7
|
+
# the class level method +get_by_id+.
|
8
|
+
# Documentation and examples can be found in the README file.
|
9
|
+
# The specs also contain lots of examples.
|
10
|
+
#
|
11
|
+
#
|
12
|
+
# = TODOs and known issues:
|
13
|
+
#
|
14
|
+
# * TODO: create a way to solve saving conflicts. Should also be possible
|
15
|
+
# to chose if only conflicts for certain variables should be handled.
|
16
|
+
#
|
17
|
+
# * TODO: If real world usage shows that it is needed then create a reload method
|
18
|
+
# that reloads the content from the db. Might be useful if some other
|
19
|
+
# process has changed the content of the document in which case the local
|
20
|
+
# copy can't be stored to the DB. Should local changes be overwritten?
|
21
|
+
# How should differences between the version on the server and the local
|
22
|
+
# version be handeled?
|
23
|
+
#
|
24
|
+
# * ISSUE:
|
25
|
+
# When creating a new document the server does not return other values
|
26
|
+
# than the new documents ID and REVISION number. The local representation
|
27
|
+
# of the created_at and updated_at attributes are therefore set right before
|
28
|
+
# the call to the server is issued. Their values are therefore not
|
29
|
+
# guaranteed to be completely in sync with the values stored on the server!
|
30
|
+
#
|
31
|
+
# * ISSUE:
|
32
|
+
# When loading multiple objects from a view using the get_from_view
|
33
|
+
# mehtod that normally would have been in a belongs_to or has_many relation,
|
34
|
+
# their relations aren't set.
|
35
|
+
#
|
36
|
+
# * ISSUE:
|
37
|
+
# Given the following scenario: B is in a has_many/has_one relationship to A
|
38
|
+
# and both have been saved to the database. B is removed from the
|
39
|
+
# relationship and A is then saved. Saving A wont save the changed version
|
40
|
+
# of B where the relationship has been ended. Therefore: the next time A
|
41
|
+
# is loaded from the database B will be loaded as a relative!
|
42
|
+
#
|
43
|
+
# Possible sollutions:
|
44
|
+
# 1) keep a list of objects that need to be saved in A so that B gets
|
45
|
+
# saved when A is saved althouh the relationship has ended.
|
46
|
+
# 2) save B as it is removed from the relationship with A.
|
47
|
+
#
|
48
|
+
# I belive 1) to be the better solution as it doesn't prematurely save B
|
49
|
+
# in cases where you don't want the changes you do to be saved to the
|
50
|
+
# database.
|
51
|
+
# MAKE SURE: that the object that is added doesn't have the same id
|
52
|
+
# and revision number ass the object that is replaced. That happens
|
53
|
+
# when a class loads its belongs_to relations! In cases like that
|
54
|
+
# the old class should NOT be tracked and saved later!
|
55
|
+
#
|
56
|
+
$:.unshift File.dirname(__FILE__)
|
57
|
+
require 'persistable/has_many_relations_array'
|
58
|
+
require 'persistable/meta_classes'
|
59
|
+
require 'persistable/overloaded_methods'
|
60
|
+
|
1
61
|
module CouchObject
|
2
|
-
module Persistable
|
3
|
-
|
4
|
-
klazz.extend(ClassMethods)
|
5
|
-
end
|
6
|
-
|
62
|
+
module Persistable
|
63
|
+
|
7
64
|
module ClassMethods
|
8
|
-
#
|
9
|
-
|
10
|
-
|
65
|
+
#
|
66
|
+
# Loads a document from the database
|
67
|
+
#
|
68
|
+
# Aliases:
|
69
|
+
# * +get+
|
70
|
+
#
|
71
|
+
# Takes:
|
72
|
+
# * +id+: the ID of the document that should be loaded
|
73
|
+
# * +db_uri+: the uri to the database. Is optional if the database has
|
74
|
+
# been defined on class level:
|
75
|
+
#
|
76
|
+
# class SomeClass
|
77
|
+
# include CouchObject::Persistable
|
78
|
+
# database 'http://localhost:5984'
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# Returns:
|
82
|
+
# * a fully initialized class of type self
|
83
|
+
#
|
84
|
+
# Raises:
|
85
|
+
# * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+
|
86
|
+
# is blank AND has not been set on class level
|
87
|
+
# * CouchObject::Errors::DocumentNotFound if the document doesn't
|
88
|
+
# exist or has been deleted
|
89
|
+
#
|
90
|
+
def get_by_id(id, db_uri = self.location)
|
91
|
+
# Raises an error if the location variable hasn't been set
|
92
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri
|
93
|
+
|
11
94
|
db = CouchObject::Database.open(db_uri)
|
12
|
-
response = db.get(id)
|
13
|
-
|
95
|
+
response = JSON.parse(db.get(id).body)
|
96
|
+
|
97
|
+
if response["error"]
|
98
|
+
case response["reason"]
|
99
|
+
when "deleted"
|
100
|
+
raise CouchObject::Errors::DocumentNotFound, "The document has been deleted"
|
101
|
+
else
|
102
|
+
raise CouchObject::Errors::DocumentNotFound, "The document could not be found"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# creates a new object and initialize all its sub objects
|
107
|
+
new_object = couch_load_object(response)
|
108
|
+
|
109
|
+
# set the storage location it was loaded from so it can be saved
|
110
|
+
# back directly without having to supply the db_uri again
|
111
|
+
new_object.instance_variable_set("@location", db_uri)
|
112
|
+
|
113
|
+
# return the new couch object
|
114
|
+
new_object
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Alias for get_by_id
|
119
|
+
#
|
120
|
+
alias get get_by_id
|
121
|
+
|
122
|
+
#
|
123
|
+
# Takes, returns and raises the same things as +get_by_id+
|
124
|
+
#
|
125
|
+
# Creates a new object that is forced into smart save mode
|
126
|
+
# although the class it is stemming from might not have smart
|
127
|
+
# saving enabled.
|
128
|
+
#
|
129
|
+
def get_with_smart_save(id, db_uri = self.location)
|
130
|
+
|
131
|
+
new_object = self.get_by_id(id, db_uri)
|
132
|
+
|
133
|
+
# Force it into smart save mode
|
134
|
+
new_object.couch_force_smart_save
|
135
|
+
|
136
|
+
# Initialize the original state.
|
137
|
+
new_object.couch_set_initial_state
|
138
|
+
|
139
|
+
new_object
|
140
|
+
end
|
141
|
+
|
142
|
+
#
|
143
|
+
# Loads all document from a given view from the database
|
144
|
+
#
|
145
|
+
# Takes:
|
146
|
+
# * +view+ (string): the name of the view to call
|
147
|
+
# * [+params+] (hash): a hash of URL query arguments supported
|
148
|
+
# by couchDB. If omitted it defaults to not use a key
|
149
|
+
# and not update the view.
|
150
|
+
# Additionally the +db_uri+ can be set as a parameter if
|
151
|
+
# it hasn't been defined at class level.
|
152
|
+
#
|
153
|
+
# Example:
|
154
|
+
#
|
155
|
+
# AppleTree.get_from_view("foo_view",
|
156
|
+
# { :db_uri => "http://localhost:5984/mydb",
|
157
|
+
# :update => false, :key => "bar"}) => Array
|
158
|
+
#
|
159
|
+
# Returns:
|
160
|
+
# * a array of initialized classes
|
161
|
+
# (if the view includes the documents full content)
|
162
|
+
#
|
163
|
+
# Raises:
|
164
|
+
# * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+
|
165
|
+
# is blank AND has not been set on class level
|
166
|
+
# * CouchObject::Errors::MissingView if the view doesn't exist
|
167
|
+
#
|
168
|
+
def get_from_view( view, params = {:key => nil,
|
169
|
+
:update => true,
|
170
|
+
:db_uri => self.location})
|
171
|
+
|
172
|
+
# Raise an error if the location variable hasn't been set
|
173
|
+
db_uri = params[:db_uri] || self.location
|
174
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri
|
175
|
+
|
176
|
+
db = CouchObject::Database.open(db_uri)
|
177
|
+
|
178
|
+
params.delete(:db_uri)
|
179
|
+
|
180
|
+
#Create a querystring with the parameters passed inn
|
181
|
+
querystring = "?"
|
182
|
+
params.each_pair do |key, value|
|
183
|
+
querystring += \
|
184
|
+
"#{key}=#{Utils.encode_querystring_parameter(value)}&"
|
185
|
+
end
|
186
|
+
querystring = querystring[0...-1]
|
187
|
+
|
188
|
+
view_with_parameters = view + querystring
|
189
|
+
|
190
|
+
objects_to_return = []
|
191
|
+
|
192
|
+
response = JSON.parse(db.get(view_with_parameters).body)
|
193
|
+
|
194
|
+
raise CouchObject::Errors::MissingView, \
|
195
|
+
"The view '#{view}' doesn't exist on the server" \
|
196
|
+
if response["error"] == "not_found"
|
197
|
+
|
198
|
+
raise CouchObject::Errors::CouchDBError, \
|
199
|
+
"CouchDB returned and error and described the problem as #{response['reason']}. \n" + \
|
200
|
+
"There might be something wrong with one of your views, or it might be missing!" \
|
201
|
+
if response["error"]
|
202
|
+
|
203
|
+
response["rows"].each do |params_for_object|
|
204
|
+
objects_to_return << couch_load_object(params_for_object["value"])
|
205
|
+
end
|
206
|
+
|
207
|
+
objects_to_return
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
protected
|
212
|
+
#
|
213
|
+
# This recursive method initializes new instances of self and
|
214
|
+
# makes sure all sub classes are also initialized.
|
215
|
+
#
|
216
|
+
# Takes:
|
217
|
+
# * A hash of parameters loaded from CoachDB
|
218
|
+
#
|
219
|
+
# Returns:
|
220
|
+
# * An fully initialized object
|
221
|
+
#
|
222
|
+
# Raises:
|
223
|
+
# * Does currently not raise any error
|
224
|
+
#
|
225
|
+
def couch_load_object(parameters)
|
226
|
+
|
227
|
+
# Getting the values that shouldn't be passed on to the initializer
|
228
|
+
id = parameters["_id"]
|
229
|
+
revision = parameters["_rev"]
|
230
|
+
created_at = parameters["created_at"]
|
231
|
+
updated_at = parameters["updated_at"]
|
232
|
+
class_type = parameters["class"]
|
233
|
+
belongs_to = parameters["belongs_to"]
|
234
|
+
|
235
|
+
new_object_from_couch = nil
|
236
|
+
|
237
|
+
# Two possible routes:
|
238
|
+
# * the class has implemented a from_couch class method, in which
|
239
|
+
# case we use it, or
|
240
|
+
# * there is no from_couch method, so we just add the attributes
|
241
|
+
# as instance variables if they aren't part of a class...
|
242
|
+
if eval("#{class_type}.respond_to?(:from_couch)")
|
243
|
+
new_object_from_couch = eval("#{class_type}.send(:from_couch," \
|
244
|
+
" parameters[\"attributes\"])")
|
245
|
+
else
|
246
|
+
new_object_from_couch = eval("#{class_type}.new")
|
247
|
+
unless parameters["attributes"] == nil
|
248
|
+
parameters["attributes"].each_key do |key|
|
249
|
+
# Check if the key value pair is a class
|
250
|
+
if parameters["attributes"][key].class == Hash && \
|
251
|
+
parameters["attributes"][key]["class"] != nil
|
252
|
+
|
253
|
+
new_object_from_couch.instance_variable_set("@#{key}", \
|
254
|
+
self.couch_load_object(parameters["attributes"][key]))
|
255
|
+
else
|
256
|
+
# Add the value to the class
|
257
|
+
new_object_from_couch.
|
258
|
+
instance_variable_set("@#{key}", \
|
259
|
+
parameters["attributes"][key])
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Sets couch_object related values
|
266
|
+
new_object_from_couch.
|
267
|
+
instance_variable_set("@revision", revision) if revision
|
268
|
+
|
269
|
+
new_object_from_couch.instance_variable_set("@id", id) if id
|
270
|
+
|
271
|
+
new_object_from_couch.instance_variable_set("@created_at", \
|
272
|
+
created_at) if eval("#{class_type}." \
|
273
|
+
"couch_object_timestamp_on_create?")
|
274
|
+
|
275
|
+
new_object_from_couch.instance_variable_set("@updated_at", \
|
276
|
+
updated_at) if eval("#{class_type}." \
|
277
|
+
"couch_object_timestamp_on_update?")
|
278
|
+
|
279
|
+
new_object_from_couch.
|
280
|
+
instance_variable_set("@belongs_to", belongs_to) \
|
281
|
+
if belongs_to
|
282
|
+
|
283
|
+
new_object_from_couch.couch_set_initial_state
|
284
|
+
|
285
|
+
# Returns the new object
|
286
|
+
new_object_from_couch
|
14
287
|
end
|
15
288
|
end
|
16
289
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
290
|
+
public
|
291
|
+
|
292
|
+
#
|
293
|
+
# Accessors for instance variables specific to the persistable
|
294
|
+
# CouchObject
|
295
|
+
#
|
296
|
+
attr_reader :id,
|
297
|
+
:revision,
|
298
|
+
:updated_at,
|
299
|
+
:created_at
|
300
|
+
|
301
|
+
#
|
302
|
+
# Returns:
|
303
|
+
# * +true+ if the object hasn't been saved
|
304
|
+
# * +false+ if the object has previously been stored or is loaded from the
|
305
|
+
# document store
|
306
|
+
#
|
307
|
+
def new?
|
308
|
+
id.nil? || revision.nil?
|
309
|
+
end
|
310
|
+
|
311
|
+
#
|
312
|
+
# Stores the initial value of the instance to a variable
|
313
|
+
# for later reference by the +unsaved_changes?+ method
|
314
|
+
#
|
315
|
+
def couch_set_initial_state
|
316
|
+
# For the unsaved_changes? instance method to work, we have to
|
317
|
+
# supply a snapshot of what the fresh object looked like.
|
318
|
+
# BUT ONLY if the user has activated the smart_save option
|
319
|
+
if use_smart_save
|
320
|
+
|
321
|
+
@couch_initial_load = true
|
322
|
+
@couch_object_original_state = to_json
|
323
|
+
@couch_initial_load = false
|
324
|
+
|
23
325
|
end
|
24
|
-
response
|
25
326
|
end
|
327
|
+
|
328
|
+
#
|
329
|
+
# Forces the instance object into smart save mode
|
330
|
+
#
|
331
|
+
def couch_force_smart_save
|
332
|
+
def self.use_smart_save
|
333
|
+
true
|
334
|
+
end
|
335
|
+
end
|
26
336
|
|
27
|
-
#
|
28
|
-
|
29
|
-
|
337
|
+
#
|
338
|
+
# Saves the object to the db_uri supplied, or if not set, to the
|
339
|
+
# location the object has previously been saved to.
|
340
|
+
#
|
341
|
+
# Takes:
|
342
|
+
# * +db_uri+ as string which is the location of the database:
|
343
|
+
# Example: http://localhost:5984/mydb
|
344
|
+
#
|
345
|
+
# Raises:
|
346
|
+
# * CouchObject::Errors::NoDatabaseLocationSet error
|
347
|
+
# if the object doesn't have a previously set location and
|
348
|
+
# the +db_uri+ is nil
|
349
|
+
#
|
350
|
+
# Returns:
|
351
|
+
# * Hash with the id and revision:
|
352
|
+
# {:id => "1234", :revision => "ABC123"}
|
353
|
+
#
|
354
|
+
# Sub methods might raise:
|
355
|
+
# * CouchObject::Errors::DatabaseSaveFailed if the save fails
|
356
|
+
#
|
357
|
+
def save(db_uri = location)
|
358
|
+
# if the location hasn't been set, set it
|
359
|
+
@location ||= db_uri
|
360
|
+
|
361
|
+
# Raises an error if the location variable hasn't been set
|
362
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless location
|
363
|
+
|
364
|
+
# If it's belongs_to relationships haven't been saved
|
365
|
+
# then it has be done first.
|
366
|
+
# Saving the master will also automatically save the child
|
367
|
+
performed_save = false
|
368
|
+
|
369
|
+
# If the belongs_to relationships haven't already been loaded,
|
370
|
+
# there is reason to believe that:
|
371
|
+
# * The object is new and doesn't have any relation set
|
372
|
+
# * The object already knows about it's belongs_to relations
|
373
|
+
# and doesn't need aditional information about them for saving
|
374
|
+
# We therefore deactivate the loading of belongs_to relations
|
375
|
+
original_state_of_load_belongs_to_relations = \
|
376
|
+
@do_not_load_belongs_to_relations
|
377
|
+
@do_not_load_belongs_to_relations = true
|
378
|
+
|
379
|
+
belongs_to.each do |what_it_belongs_to|
|
380
|
+
master_class = self.send(what_it_belongs_to)
|
381
|
+
unless master_class == nil || !master_class.new?
|
382
|
+
master_class.save
|
383
|
+
performed_save = true
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# Reset the do_not_load_belongs_to_relations variable to its
|
388
|
+
# original state
|
389
|
+
@do_not_load_belongs_to_relations = \
|
390
|
+
original_state_of_load_belongs_to_relations
|
391
|
+
|
392
|
+
# If none of the master classes were saved, meaning they weren't new
|
393
|
+
# or didn't exist, then we have to manually save this object.
|
394
|
+
couch_perform_save unless performed_save
|
395
|
+
|
396
|
+
{:id => @id, :revision => @revision}
|
397
|
+
|
398
|
+
end
|
399
|
+
protected
|
400
|
+
def couch_perform_save
|
401
|
+
perform_callback(:before_save)
|
402
|
+
|
403
|
+
# Go... action
|
404
|
+
the_return_value = new? ? couch_create : couch_update
|
405
|
+
|
406
|
+
# Save all the has_many relations
|
407
|
+
# But only the has_many relations that have already
|
408
|
+
# been loaded! No need to load the relations from
|
409
|
+
# the db to save them back again!
|
410
|
+
state_before_wants_to_load_relations = @do_not_load_has_many_relations
|
411
|
+
@do_not_load_has_many_relations = true
|
412
|
+
|
413
|
+
# We thread the save process in case the relations do
|
414
|
+
# some funky time consuming stuff in their call backs
|
415
|
+
threads = []
|
416
|
+
has_many.each do |thing_it_has_many_of|
|
417
|
+
self.send(thing_it_has_many_of).each do |related_object|
|
418
|
+
threads << Thread.new(related_object) do |object_to_save|
|
419
|
+
object_to_save.save(location)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
threads.each {|thr| thr.join}
|
424
|
+
|
425
|
+
# Save all the has_one relations
|
426
|
+
has_one.each do |thing_it_has_one_of|
|
427
|
+
related_object = self.send(thing_it_has_one_of)
|
428
|
+
|
429
|
+
unless related_object == nil
|
430
|
+
related_object.save(location)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# Reset the do_not_load_has_many_relations variable
|
435
|
+
# to it's original state
|
436
|
+
@do_not_load_has_many_relations = state_before_wants_to_load_relations
|
437
|
+
|
438
|
+
perform_callback(:after_save)
|
439
|
+
|
440
|
+
return the_return_value
|
441
|
+
end
|
442
|
+
|
443
|
+
#
|
444
|
+
# Saves the class as a new document in the database
|
445
|
+
#
|
446
|
+
# Returns:
|
447
|
+
# * CouchObject::Response
|
448
|
+
#
|
449
|
+
# Raises:
|
450
|
+
# * CouchObject::Errors::DatabaseSaveFailed if the save fails
|
451
|
+
#
|
452
|
+
def couch_create
|
453
|
+
perform_callback(:before_create)
|
454
|
+
|
455
|
+
db = CouchObject::Database.open(location)
|
456
|
+
|
457
|
+
json_value = self.to_json
|
458
|
+
|
459
|
+
unless (response = db.post("", json_value)).to_document["error"]
|
460
|
+
response_document = response.to_document
|
461
|
+
@id = response_document.id
|
462
|
+
@revision = response_document.revision
|
463
|
+
@created_at = Time.now if self.
|
464
|
+
class::couch_object_timestamp_on_create?
|
465
|
+
@updated_at = Time.now if self.
|
466
|
+
class::couch_object_timestamp_on_update?
|
467
|
+
|
468
|
+
# If the user has activated smart_save, we should set the state
|
469
|
+
# to the new contents of this instance!
|
470
|
+
@couch_object_original_state = json_value if use_smart_save
|
471
|
+
|
472
|
+
perform_callback(:after_create)
|
473
|
+
|
474
|
+
# Returns a hash with the ID and Revision
|
475
|
+
{:id => @id, :revision => @revision}
|
476
|
+
|
477
|
+
else
|
478
|
+
|
479
|
+
raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
|
480
|
+
"couldn't be created.\n" + \
|
481
|
+
"CouchDB reported: #{response.to_document["error"]}"
|
482
|
+
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
#
|
487
|
+
# Updates a document in the database based on the id and revision
|
488
|
+
#
|
489
|
+
# Returns:
|
490
|
+
# * CouchObject::Response
|
491
|
+
#
|
492
|
+
# Raises:
|
493
|
+
# * CouchObject::Errors::DatabaseSaveFailed if the update fails
|
494
|
+
#
|
495
|
+
def couch_update
|
496
|
+
|
497
|
+
# Only save if it has unsaved changes!
|
498
|
+
if unsaved_changes?
|
499
|
+
|
500
|
+
perform_callback(:before_update)
|
501
|
+
|
502
|
+
# Please notice the following:
|
503
|
+
# The response from CouchDB only includes the revision number
|
504
|
+
# and the ID so the updated_at value in the document store
|
505
|
+
# differs from the updated_at value in the class
|
506
|
+
@updated_at = Time.now if self.
|
507
|
+
class::couch_object_timestamp_on_update?
|
508
|
+
|
509
|
+
db = CouchObject::Database.open(location)
|
510
|
+
|
511
|
+
json_value = self.to_json
|
512
|
+
|
513
|
+
unless (response = db.put(id, json_value)).to_document["error"]
|
514
|
+
|
515
|
+
@revision = response.to_document.revision
|
516
|
+
|
517
|
+
# If the user has activated smart_save, we should set the state
|
518
|
+
# to the new contents of this instance!
|
519
|
+
@couch_object_original_state = json_value if use_smart_save
|
520
|
+
|
521
|
+
perform_callback(:after_update)
|
522
|
+
|
523
|
+
# Returns a hash with the ID and Revision
|
524
|
+
{:id => @id, :revision => @revision}
|
525
|
+
|
526
|
+
else
|
527
|
+
|
528
|
+
raise CouchObject::Errors::DatabaseSaveFailed, "The document " + \
|
529
|
+
"couldn't be updated.\n" + \
|
530
|
+
"The reason might be a revision number conflict.\n" + \
|
531
|
+
"CouchDB reported: #{response.to_document["reason"]}"
|
532
|
+
|
533
|
+
end
|
534
|
+
|
535
|
+
|
536
|
+
end
|
537
|
+
|
30
538
|
end
|
539
|
+
|
540
|
+
|
541
|
+
public
|
542
|
+
#
|
543
|
+
# Classes WITH smart_save activated:
|
544
|
+
# Any instance should be able to know if it has unsaved changes or not.
|
545
|
+
# When an instance is loaded from the DB it creates a snapshot of what
|
546
|
+
# its variables contain. Based on a comparison between the snapshot
|
547
|
+
# and the contents of the instance this method returns true or false.
|
548
|
+
#
|
549
|
+
# Classes WITHOUT smart_save activated:
|
550
|
+
# Will always return true regardless of what state it is in
|
551
|
+
#
|
552
|
+
# A new object will always return true
|
553
|
+
#
|
554
|
+
# Returns:
|
555
|
+
# * true: if it has changes that haven't been saved to the database
|
556
|
+
# * fase: if the nothing has changed since it was loaded from the
|
557
|
+
# database.
|
558
|
+
#
|
559
|
+
def unsaved_changes?
|
560
|
+
return true if new?
|
561
|
+
return true unless use_smart_save
|
562
|
+
|
563
|
+
@couch_object_original_state == self.to_json ? false : true
|
564
|
+
|
565
|
+
end
|
566
|
+
|
567
|
+
#
|
568
|
+
# Any instance should be able to delete itself
|
569
|
+
#
|
570
|
+
# Takes:
|
571
|
+
# * +db_uri+ if not set in the location variable
|
572
|
+
#
|
573
|
+
# Returns:
|
574
|
+
# * true on success
|
575
|
+
# * false on failure
|
576
|
+
#
|
577
|
+
# Note:
|
578
|
+
# * it also deletes all has_many relations from the database
|
579
|
+
# * it removes itself from object it belongs to
|
580
|
+
#
|
581
|
+
# Raises:
|
582
|
+
# * CouchObject::Errors::NoDatabaseLocationSet if +db_uri+
|
583
|
+
# is blank AND has not been set on class level
|
584
|
+
#
|
585
|
+
def delete(db_uri = location)
|
586
|
+
|
587
|
+
perform_callback(:before_delete)
|
588
|
+
|
589
|
+
unless new?
|
590
|
+
# Raises an error if the location variable hasn't been set
|
591
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless db_uri
|
592
|
+
|
593
|
+
db = CouchObject::Database.open(db_uri)
|
594
|
+
|
595
|
+
# Removes itself from the database
|
596
|
+
db.delete(id, revision)
|
597
|
+
end
|
598
|
+
|
599
|
+
# Remove all relations
|
600
|
+
has_many.each do |what_it_has|
|
601
|
+
self.send(what_it_has).dup.each do |related_object|
|
602
|
+
related_object.delete
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
# Remove the relationship with it's has many master object
|
607
|
+
belongs_to.each do |what_it_belongs_to|
|
608
|
+
self_belongs_to_classtype = what_it_belongs_to
|
609
|
+
|
610
|
+
self_belongs_to_classtype_as = self.
|
611
|
+
send("belongs_to_#{what_it_belongs_to}_as")
|
612
|
+
|
613
|
+
self_belongs_to_class = self.send(self_belongs_to_classtype) unless \
|
614
|
+
self_belongs_to_classtype.nil?
|
615
|
+
|
616
|
+
self_belongs_to_class.end_relationsship_with(self,
|
617
|
+
self_belongs_to_classtype_as) unless self_belongs_to_class.nil?
|
618
|
+
|
619
|
+
# Remove the relationship with it's belongs_to master from itself
|
620
|
+
remove_call = "#{self_belongs_to_classtype.to_s}" + \
|
621
|
+
"_without_call_back="
|
622
|
+
self.send(remove_call.to_sym, nil) if self_belongs_to_classtype
|
623
|
+
|
624
|
+
end
|
625
|
+
|
626
|
+
# Reset itself
|
627
|
+
@id = nil
|
628
|
+
@revision = nil
|
629
|
+
@location = nil
|
630
|
+
|
631
|
+
perform_callback(:after_delete)
|
632
|
+
|
633
|
+
true
|
31
634
|
|
32
|
-
# the Couch document id of this object
|
33
|
-
def id
|
34
|
-
@id
|
35
635
|
end
|
36
636
|
|
37
|
-
#
|
637
|
+
#
|
638
|
+
# Breaks relations if existing
|
639
|
+
#
|
640
|
+
# Takes:
|
641
|
+
# * +undesired_object+ as a reference to the object the
|
642
|
+
# relationship should be broken with
|
643
|
+
# * +which_is_stored_as+ (string) which is what the relation
|
644
|
+
# is stored as.
|
645
|
+
#
|
646
|
+
def end_relationsship_with(undersired_object, which_is_stored_as)
|
647
|
+
self.send(which_is_stored_as.to_sym).perform_remove(undersired_object)
|
648
|
+
end
|
649
|
+
|
650
|
+
#
|
651
|
+
# Sets the location variable manually
|
652
|
+
#
|
653
|
+
# Takes:
|
654
|
+
# * +db_uri+ as string which is the location of the database:
|
655
|
+
# Example: http://localhost:5984/mydb
|
656
|
+
#
|
657
|
+
def set_location=(db_uri)
|
658
|
+
@location = db_uri == "" ? nil : db_uri
|
659
|
+
end
|
660
|
+
alias set_storage_location= set_location=
|
661
|
+
|
662
|
+
#
|
663
|
+
# serializes this object into JSON
|
664
|
+
#
|
665
|
+
# Returns:
|
666
|
+
# * The values of the class in json format
|
667
|
+
#
|
668
|
+
# Example
|
669
|
+
# {"class":"Bike","attributes":{"wheels":2}}
|
670
|
+
#
|
38
671
|
def to_json
|
39
|
-
|
40
|
-
|
672
|
+
parameters = {}
|
673
|
+
|
674
|
+
parameters["class"] = self.class
|
675
|
+
|
676
|
+
if respond_to?(:to_couch)
|
677
|
+
parameters["attributes"] = self.to_couch
|
678
|
+
else
|
679
|
+
# Find all the instance variables and add them to the
|
680
|
+
# attributes parameter
|
681
|
+
p_attributes = {}
|
682
|
+
instance_variables = \
|
683
|
+
self.instance_variables - ["@location",
|
684
|
+
"@created_at",
|
685
|
+
"@updated_at",
|
686
|
+
"@id",
|
687
|
+
"@revision",
|
688
|
+
"@do_not_load_has_many_relations",
|
689
|
+
"@do_not_load_belongs_to_relations",
|
690
|
+
"@couch_object_original_state",
|
691
|
+
"@couch_initial_load",
|
692
|
+
"@belongs_to"]
|
693
|
+
|
694
|
+
# We also have to remove all the objects that are related
|
695
|
+
# through belongs_to and has_many relations. They have to be called
|
696
|
+
# saved separately
|
697
|
+
has.each do |thing_it_has|
|
698
|
+
instance_variables = instance_variables - \
|
699
|
+
["@couch_object_#{thing_it_has.to_s}"]
|
700
|
+
end
|
701
|
+
|
702
|
+
belongs_to.each do |things_it_belongs_to|
|
703
|
+
instance_variables = instance_variables - \
|
704
|
+
["@couch_object_#{things_it_belongs_to.to_s}"]
|
705
|
+
end
|
706
|
+
|
707
|
+
instance_variables.each do |var|
|
708
|
+
p_attributes[var[1..(var.length)]] = self.instance_variable_get(var)
|
709
|
+
end
|
710
|
+
parameters["attributes"] = p_attributes
|
711
|
+
end
|
712
|
+
|
713
|
+
parameters["updated_at"] = Time.now \
|
714
|
+
if self.class::couch_object_timestamp_on_update?
|
715
|
+
|
716
|
+
unless new?
|
717
|
+
parameters["_id"] = id
|
718
|
+
parameters["_rev"] = revision
|
719
|
+
parameters["created_at"] = created_at \
|
720
|
+
if self.class::couch_object_timestamp_on_create?
|
721
|
+
else
|
722
|
+
parameters["created_at"] = Time.now \
|
723
|
+
if self.class::couch_object_timestamp_on_create?
|
724
|
+
end
|
725
|
+
|
726
|
+
# If it is in belongs_to relationship(s), then that fact
|
727
|
+
# has to be stored in the database
|
728
|
+
if belongs_to != [] and !@couch_initial_load # the couch_initial_load
|
729
|
+
# is to get the initial
|
730
|
+
# state of the object
|
731
|
+
# without loading the
|
732
|
+
# belongs_to relations
|
733
|
+
# which would start an
|
734
|
+
# infinite loop.
|
735
|
+
# NOTE: this is only for
|
736
|
+
# cases where smart_save
|
737
|
+
# has been activated.
|
738
|
+
|
739
|
+
# No reason to load the belongs_to relations if they haven't
|
740
|
+
# already been loaded from the database!
|
741
|
+
original_state_of_load_belongs_to_relations = \
|
742
|
+
@do_not_load_belongs_to_relations
|
743
|
+
@do_not_load_belongs_to_relations = true
|
744
|
+
|
745
|
+
what_it_belongs_to = {}
|
746
|
+
self.send(:belongs_to).each do |relation|
|
747
|
+
as_what = self.send("belongs_to_#{relation}_as")
|
748
|
+
object_it_belongs_to = self.send(relation)
|
749
|
+
|
750
|
+
# Unless the relation is unset, in which case it will be of
|
751
|
+
# type NilClass, set it.
|
752
|
+
what_it_belongs_to[as_what.to_s] = object_it_belongs_to.id || "new" \
|
753
|
+
unless object_it_belongs_to.class == NilClass
|
754
|
+
end
|
755
|
+
|
756
|
+
# Reset the value so they are loaded the next time when needed
|
757
|
+
# if that is what the user wants.
|
758
|
+
@do_not_load_belongs_to_relations = \
|
759
|
+
original_state_of_load_belongs_to_relations
|
760
|
+
|
761
|
+
|
762
|
+
# We have to make sure the @belongs_to variable contains all changes
|
763
|
+
# and all the original values for the keys that haven't changed/
|
764
|
+
# relations that haven't been loaded
|
765
|
+
original_belongs_to = @belongs_to || {}
|
766
|
+
@belongs_to = what_it_belongs_to
|
767
|
+
times_through = 1
|
768
|
+
|
769
|
+
# LOOP 1... see below for problem description
|
770
|
+
original_belongs_to.each_pair do |key, value|
|
771
|
+
times_through += 1
|
772
|
+
@belongs_to[key.to_s] = value unless @belongs_to[key.to_s]
|
773
|
+
end
|
774
|
+
|
775
|
+
# FIXME:
|
776
|
+
# Now... this is a really hacky way to solve this problem
|
777
|
+
# and should be improved... Feel free to come up with sollutions
|
778
|
+
# Case:
|
779
|
+
# If it has a belongs_to relationship that is new and therefore
|
780
|
+
# doesn't have an ID it would normally be written in the @belongs_to
|
781
|
+
# variable as nil. The problem is that if self has previously
|
782
|
+
# been saved with another parent object this ID would still come
|
783
|
+
# through in the @belongs_to variable updater (see "LOOP 1" above).
|
784
|
+
# We therefore assign the ID "new" to all unsaved relations, which we
|
785
|
+
# now have to nilify. If we don't the smart save wont work for this
|
786
|
+
# type of cases.
|
787
|
+
|
788
|
+
end
|
789
|
+
|
790
|
+
parameters["belongs_to"] = @belongs_to
|
791
|
+
parameters.delete("belongs_to") if @belongs_to == {} or @belongs_to == nil
|
792
|
+
|
793
|
+
# if @couch_initial_load && @belongs_to
|
794
|
+
|
795
|
+
begin
|
796
|
+
parameters.to_json
|
797
|
+
rescue JSON::GeneratorError
|
798
|
+
# All strings aren't encoded properly, so we have to force them into
|
799
|
+
# UTF-8.
|
800
|
+
# FIXME: The kconv library has some weird artefacts though where
|
801
|
+
# a lot of Norwegian (Scandianavian?) letters get turned into
|
802
|
+
# asian characters of some sort!
|
803
|
+
CouchObject::Utils::decode_strings(parameters).to_json
|
804
|
+
end
|
805
|
+
|
806
|
+
end
|
807
|
+
|
808
|
+
|
809
|
+
|
810
|
+
#
|
811
|
+
# Sometimes you might want to add an object to
|
812
|
+
# a has_many relation without interacting with the other relations at all.
|
813
|
+
# In cases like that, when loading all the relations would just
|
814
|
+
# cause unnecessary traffic to the database, you can tell the object
|
815
|
+
# not to load it has_many relations using this method
|
816
|
+
#
|
817
|
+
def do_not_load_has_many_relations
|
818
|
+
@do_not_load_has_many_relations = true
|
819
|
+
end
|
820
|
+
def do_load_has_many_relations
|
821
|
+
@do_not_load_has_many_relations = false
|
822
|
+
end
|
823
|
+
alias do_not_load_has_one_relations do_not_load_has_many_relations
|
824
|
+
alias do_load_has_one_relations do_load_has_many_relations
|
825
|
+
alias do_not_load_has_one_relation do_not_load_has_many_relations
|
826
|
+
alias do_load_has_one_relation do_load_has_many_relations
|
827
|
+
|
828
|
+
#
|
829
|
+
# If you need to access the belongs_to variable without loading the
|
830
|
+
# relation if it hasn't already been loaded, you can call the instance
|
831
|
+
# method +do_not_load_belongs_to_relations+. To reactivate loading so
|
832
|
+
# the relation is loaded the next time it is needed, call the instance
|
833
|
+
# method +do_load_belongs_to_relations+.
|
834
|
+
#
|
835
|
+
def do_not_load_belongs_to_relations
|
836
|
+
@do_not_load_belongs_to_relations = true
|
41
837
|
end
|
838
|
+
def do_load_belongs_to_relations
|
839
|
+
@do_not_load_belongs_to_relations = false
|
840
|
+
end
|
841
|
+
alias do_not_load_has_many_relation do_not_load_has_many_relations
|
842
|
+
alias do_load_belongs_to_relation do_load_belongs_to_relations
|
843
|
+
|
844
|
+
|
845
|
+
protected
|
846
|
+
#
|
847
|
+
# Loads has_many relations
|
848
|
+
#
|
849
|
+
def couch_load_has_many_relations(which_relation)
|
850
|
+
# If it is a new and unsaved object it wont have
|
851
|
+
# relations in the DB. Return a blank array
|
852
|
+
results = CouchObject::Persistable::HasManyRelation.new(self)
|
853
|
+
|
854
|
+
# If it is loading a has_one relation, the relation name has to
|
855
|
+
# be changed
|
856
|
+
which_relation = which_relation[8..-1] \
|
857
|
+
if which_relation[0..7] == "has_one_"
|
858
|
+
|
859
|
+
return results if new? || @do_not_load_has_many_relations
|
42
860
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
861
|
+
view_name = "couch_object_has_many_relations"
|
862
|
+
|
863
|
+
begin
|
864
|
+
|
865
|
+
new_objects = self.class.
|
866
|
+
get_from_view("_view/#{view_name}/related_documents", \
|
867
|
+
{:key => [self.id, which_relation],
|
868
|
+
:db_uri => location} )
|
869
|
+
|
870
|
+
# Disable the call back function of HasManyRelations so we don't
|
871
|
+
# get an infinite loop
|
872
|
+
results.disable_call_back_on_add
|
873
|
+
|
874
|
+
new_objects.each do |new_object|
|
875
|
+
results << new_object
|
876
|
+
end
|
877
|
+
|
878
|
+
# Reanable the call back function again so it works like normal
|
879
|
+
# from now on
|
880
|
+
results.enable_call_back_on_add
|
881
|
+
|
882
|
+
rescue CouchObject::Errors::MissingView, CouchObject::Errors::CouchDBError
|
883
|
+
|
884
|
+
# The view doesn't exist. It means this is the first
|
885
|
+
# time this script is used for a given database, or the user
|
886
|
+
# has deleted the view
|
887
|
+
view_code_query = JSON.unparse(
|
888
|
+
{
|
889
|
+
"_id" => "_design/#{view_name}",
|
890
|
+
"language" => "text/javascript",
|
891
|
+
"views" =>
|
892
|
+
{
|
893
|
+
"related_documents" => "function(doc){if (doc.belongs_to){" + \
|
894
|
+
"for (var i in doc.belongs_to) {map([doc.belongs_to[i],i], " + \
|
895
|
+
"doc);}}}"
|
896
|
+
}
|
897
|
+
}
|
898
|
+
)
|
899
|
+
|
900
|
+
db = CouchObject::Database.open(location)
|
901
|
+
|
902
|
+
if (response = db.put("_design%2F#{view_name}", \
|
903
|
+
view_code_query))
|
904
|
+
results = couch_load_has_many_relations(which_relation)
|
905
|
+
else
|
906
|
+
raise CouchObject::Errors::StandardError, "Couldn't create the view..."
|
907
|
+
end
|
47
908
|
end
|
48
|
-
|
909
|
+
|
910
|
+
return results
|
49
911
|
end
|
50
912
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
913
|
+
#
|
914
|
+
# Loads the belongs_to relation if the class has a previously
|
915
|
+
# save relation
|
916
|
+
#
|
917
|
+
def couch_load_belongs_to_relation(that_is_called)
|
918
|
+
|
919
|
+
return nil if new? || @do_not_load_belongs_to_relations
|
920
|
+
|
921
|
+
# If it doesn't have a belongs to ID then there is no
|
922
|
+
# related object in the database
|
923
|
+
if @belongs_to && @belongs_to[that_is_called]
|
924
|
+
|
925
|
+
# If it is a new and unsaved object it wont have
|
926
|
+
# relations in the DB. Return a blank array
|
927
|
+
return nil if new?
|
928
|
+
|
929
|
+
# Raises an error if the location variable hasn't been set
|
930
|
+
raise CouchObject::Errors::NoDatabaseLocationSet unless location
|
931
|
+
|
932
|
+
db = CouchObject::Database.open(location)
|
933
|
+
|
934
|
+
class_self_belongs_to = self.
|
935
|
+
class.get_by_id(@belongs_to[that_is_called])
|
936
|
+
|
937
|
+
# We have to add self to the other end of the relation
|
938
|
+
# Adds itself to the relationship
|
939
|
+
masters_relation = class_self_belongs_to.send(that_is_called)
|
940
|
+
|
941
|
+
if masters_relation.class == CouchObject::Persistable::HasManyRelation
|
942
|
+
# it is a has_many relations
|
943
|
+
# remove the object in the relation that is the freshly loaded
|
944
|
+
# copy of self, and then add self instead.
|
945
|
+
masters_relation.each do |has_one|
|
946
|
+
if has_one.id == self.id && has_one.revision == self.revision
|
947
|
+
masters_relation.perform_remove(has_one)
|
948
|
+
break
|
949
|
+
end
|
950
|
+
end
|
951
|
+
masters_relation.disable_call_back_on_add
|
952
|
+
masters_relation << self
|
953
|
+
masters_relation.enable_call_back_on_add
|
954
|
+
else
|
955
|
+
# it is a has_one relation, just add itself
|
956
|
+
class_self_belongs_to.send("#{that_is_called}=".to_sym, self)
|
957
|
+
end
|
958
|
+
|
959
|
+
return class_self_belongs_to
|
960
|
+
|
961
|
+
else
|
962
|
+
nil
|
55
963
|
end
|
56
|
-
alias_method :to_s, :message
|
57
964
|
end
|
965
|
+
|
966
|
+
#
|
967
|
+
# Performs callbacks before and after these events:
|
968
|
+
# * create
|
969
|
+
# * update
|
970
|
+
# * save
|
971
|
+
# * delete
|
972
|
+
#
|
973
|
+
def perform_callback(the_callback)
|
974
|
+
self.send(the_callback) if self.respond_to?(the_callback)
|
975
|
+
end
|
58
976
|
end
|
59
977
|
end
|