couchrest_model 2.0.0.beta2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +8 -0
- data/Gemfile +1 -1
- data/README.md +1 -1
- data/Rakefile +9 -24
- data/VERSION +1 -1
- data/couchrest_model.gemspec +7 -5
- data/history.md +17 -1
- data/lib/couchrest/model/associations.rb +16 -11
- data/lib/couchrest/model/base.rb +17 -15
- data/lib/couchrest/model/casted_array.rb +7 -1
- data/lib/couchrest/model/core_extensions/time_parsing.rb +0 -23
- data/lib/couchrest/model/design.rb +282 -0
- data/lib/couchrest/model/designs/design_mapper.rb +79 -0
- data/lib/couchrest/model/designs/view.rb +9 -6
- data/lib/couchrest/model/designs.rb +37 -70
- data/lib/couchrest/model/persistence.rb +5 -5
- data/lib/couchrest/model/properties.rb +5 -16
- data/lib/couchrest/model/property.rb +34 -16
- data/lib/couchrest/model/translation.rb +22 -0
- data/lib/couchrest/model/typecast.rb +54 -43
- data/lib/couchrest/model/utils/migrate.rb +106 -0
- data/lib/couchrest_model.rb +4 -2
- data/lib/tasks/migrations.rake +5 -5
- data/spec/fixtures/models/course.rb +1 -0
- data/spec/fixtures/models/designs.rb +22 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/unit/assocations_spec.rb +7 -0
- data/spec/unit/base_spec.rb +3 -1
- data/spec/unit/{designs/design_spec.rb → design_spec.rb} +6 -6
- data/spec/unit/designs/design_mapper_spec.rb +124 -0
- data/spec/unit/designs/view_spec.rb +30 -4
- data/spec/unit/designs_spec.rb +5 -140
- data/spec/unit/dirty_spec.rb +15 -1
- data/spec/unit/embeddable_spec.rb +2 -2
- data/spec/unit/property_spec.rb +70 -28
- data/spec/unit/translations_spec.rb +31 -0
- data/spec/unit/typecast_spec.rb +99 -19
- data/spec/unit/utils/migrate_spec.rb +25 -0
- metadata +43 -19
- data/lib/couchrest/model/designs/design.rb +0 -284
- data/lib/couchrest/model/migrate.rb +0 -92
@@ -1,284 +0,0 @@
|
|
1
|
-
|
2
|
-
module CouchRest
|
3
|
-
module Model
|
4
|
-
module Designs
|
5
|
-
|
6
|
-
class Design < ::CouchRest::Design
|
7
|
-
|
8
|
-
# The model Class that this design belongs to and method name
|
9
|
-
attr_accessor :model, :method_name
|
10
|
-
|
11
|
-
# Can this design save itself to the database?
|
12
|
-
# If false, the design will be loaded automatically before a view is executed.
|
13
|
-
attr_accessor :auto_update
|
14
|
-
|
15
|
-
|
16
|
-
# Instantiate a new design document for this model
|
17
|
-
def initialize(model, prefix = nil)
|
18
|
-
self.model = model
|
19
|
-
self.method_name = self.class.method_name(prefix)
|
20
|
-
suffix = prefix ? "_#{prefix}" : ''
|
21
|
-
self["_id"] = "_design/#{model.to_s}#{suffix}"
|
22
|
-
apply_defaults
|
23
|
-
end
|
24
|
-
|
25
|
-
def sync(db = nil)
|
26
|
-
if auto_update
|
27
|
-
db ||= database
|
28
|
-
if cache_checksum(db) != checksum
|
29
|
-
sync!(db)
|
30
|
-
set_cache_checksum(db, checksum)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
self
|
34
|
-
end
|
35
|
-
|
36
|
-
def sync!(db = nil)
|
37
|
-
db ||= database
|
38
|
-
|
39
|
-
# Load up the last copy. We never blindly overwrite the remote copy
|
40
|
-
# as it may contain views that are not used or known about by
|
41
|
-
# our model.
|
42
|
-
doc = load_from_database(db)
|
43
|
-
|
44
|
-
if !doc || doc['couchrest-hash'] != checksum
|
45
|
-
# We need to save something
|
46
|
-
if doc
|
47
|
-
# Different! Update.
|
48
|
-
doc.merge!(to_hash)
|
49
|
-
else
|
50
|
-
# No previous doc, use a *copy* of our version.
|
51
|
-
# Using a copy prevents reverse updates.
|
52
|
-
doc = to_hash.dup
|
53
|
-
end
|
54
|
-
db.save_doc(doc)
|
55
|
-
end
|
56
|
-
|
57
|
-
self
|
58
|
-
end
|
59
|
-
|
60
|
-
# Migrate the design document preventing downtime on a production
|
61
|
-
# system. Typically this will be used when auto updates are disabled.
|
62
|
-
#
|
63
|
-
# Steps taken are:
|
64
|
-
#
|
65
|
-
# 1. Compare the checksum with the current version
|
66
|
-
# 2. If different, create a new design doc with timestamp
|
67
|
-
# 3. Wait until the view returns a result
|
68
|
-
# 4. Copy over the original design doc
|
69
|
-
#
|
70
|
-
# If a block is provided, it will be called with the result of the migration:
|
71
|
-
#
|
72
|
-
# * :no_change - Nothing performed as there are no changes.
|
73
|
-
# * :created - Add a new design doc as non existed
|
74
|
-
# * :migrated - Migrated the existing design doc.
|
75
|
-
#
|
76
|
-
# This can be used for progressivly printing the results of the migration.
|
77
|
-
#
|
78
|
-
# After completion, either a "cleanup" Proc object will be provided to finalize
|
79
|
-
# the process and copy the document into place, or simply nil if no cleanup is
|
80
|
-
# required. For example:
|
81
|
-
#
|
82
|
-
# print "Synchronising Cat model designs: "
|
83
|
-
# callback = Cat.design_doc.migrate do |res|
|
84
|
-
# puts res.to_s
|
85
|
-
# end
|
86
|
-
# if callback
|
87
|
-
# puts "Cleaning up."
|
88
|
-
# callback.call
|
89
|
-
# end
|
90
|
-
#
|
91
|
-
def migrate(db = nil, &block)
|
92
|
-
db ||= database
|
93
|
-
doc = load_from_database(db)
|
94
|
-
cleanup = nil
|
95
|
-
id = self['_id']
|
96
|
-
|
97
|
-
if !doc
|
98
|
-
# no need to migrate, just save it
|
99
|
-
new_doc = to_hash.dup
|
100
|
-
db.save_doc(new_doc)
|
101
|
-
|
102
|
-
result = :created
|
103
|
-
elsif doc['couchrest-hash'] != checksum
|
104
|
-
id += "_migration"
|
105
|
-
|
106
|
-
# Delete current migration if there is one
|
107
|
-
old_migration = load_from_database(db, id)
|
108
|
-
db.delete_doc(old_migration) if old_migration
|
109
|
-
|
110
|
-
# Save new design doc
|
111
|
-
new_doc = doc.merge(to_hash)
|
112
|
-
new_doc['_id'] = id
|
113
|
-
new_doc.delete('_rev')
|
114
|
-
db.save_doc(new_doc)
|
115
|
-
|
116
|
-
# Proc definition to copy the migration doc over the original
|
117
|
-
cleanup = Proc.new do
|
118
|
-
db.copy_doc(new_doc, doc)
|
119
|
-
db.delete_doc(new_doc)
|
120
|
-
self
|
121
|
-
end
|
122
|
-
|
123
|
-
result = :migrated
|
124
|
-
else
|
125
|
-
# Already up to date
|
126
|
-
result = :no_change
|
127
|
-
end
|
128
|
-
|
129
|
-
if new_doc && !new_doc['views'].empty?
|
130
|
-
# Create a view query and send
|
131
|
-
name = new_doc['views'].keys.first
|
132
|
-
view = new_doc['views'][name]
|
133
|
-
params = {:limit => 1}
|
134
|
-
params[:reduce] = false if view['reduce']
|
135
|
-
db.view("#{id}/_view/#{name}", params) do |res|
|
136
|
-
# Block to use streamer!
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
# Provide the result in block
|
141
|
-
yield result if block_given?
|
142
|
-
|
143
|
-
cleanup
|
144
|
-
end
|
145
|
-
|
146
|
-
# Perform a single migration and inmediatly request a cleanup operation:
|
147
|
-
#
|
148
|
-
# print "Synchronising Cat model designs: "
|
149
|
-
# Cat.design_doc.migrate! do |res|
|
150
|
-
# puts res.to_s
|
151
|
-
# end
|
152
|
-
#
|
153
|
-
def migrate!(db = nil, &block)
|
154
|
-
callback = migrate(db, &block)
|
155
|
-
if callback.is_a?(Proc)
|
156
|
-
callback.call
|
157
|
-
else
|
158
|
-
callback
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def checksum
|
163
|
-
sum = self['couchrest-hash']
|
164
|
-
if sum && (@_original_hash == to_hash)
|
165
|
-
sum
|
166
|
-
else
|
167
|
-
checksum!
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def database
|
172
|
-
model.database
|
173
|
-
end
|
174
|
-
|
175
|
-
# Override the default #uri method for one that accepts
|
176
|
-
# the current database.
|
177
|
-
# This is used by the caching code.
|
178
|
-
def uri(db = database)
|
179
|
-
"#{db.root}/#{self['_id']}"
|
180
|
-
end
|
181
|
-
|
182
|
-
|
183
|
-
######## VIEW HANDLING ########
|
184
|
-
|
185
|
-
# Create a new view object.
|
186
|
-
# This overrides the normal CouchRest Design view method
|
187
|
-
def view(name, opts = {})
|
188
|
-
CouchRest::Model::Designs::View.new(self, model, opts, name)
|
189
|
-
end
|
190
|
-
|
191
|
-
# Helper method to provide a list of all the views
|
192
|
-
def view_names
|
193
|
-
self['views'].keys
|
194
|
-
end
|
195
|
-
|
196
|
-
def has_view?(name)
|
197
|
-
view_names.include?(name.to_s)
|
198
|
-
end
|
199
|
-
|
200
|
-
# Add the specified view to the design doc the definition was made in
|
201
|
-
# and create quick access methods in the model.
|
202
|
-
def create_view(name, opts = {})
|
203
|
-
View.define_and_create(self, name, opts)
|
204
|
-
end
|
205
|
-
|
206
|
-
######## FILTER HANDLING ########
|
207
|
-
|
208
|
-
def create_filter(name, function)
|
209
|
-
filters = (self['filters'] ||= {})
|
210
|
-
filters[name.to_s] = function
|
211
|
-
end
|
212
|
-
|
213
|
-
protected
|
214
|
-
|
215
|
-
def load_from_database(db = database, id = nil)
|
216
|
-
id ||= self['_id']
|
217
|
-
db.get(id)
|
218
|
-
rescue RestClient::ResourceNotFound
|
219
|
-
nil
|
220
|
-
end
|
221
|
-
|
222
|
-
# Calculate and update the checksum of the Design document.
|
223
|
-
# Used for ensuring the latest version has been sent to the database.
|
224
|
-
#
|
225
|
-
# This will generate an flatterned, ordered array of all the elements of the
|
226
|
-
# design document, convert to string then generate an MD5 Hash. This should
|
227
|
-
# result in a consisitent Hash accross all platforms.
|
228
|
-
#
|
229
|
-
def checksum!
|
230
|
-
# Get a deep copy of hash to compare with
|
231
|
-
@_original_hash = Marshal.load(Marshal.dump(to_hash))
|
232
|
-
# create a copy of basic elements
|
233
|
-
base = self.dup
|
234
|
-
base.delete('_id')
|
235
|
-
base.delete('_rev')
|
236
|
-
base.delete('couchrest-hash')
|
237
|
-
result = nil
|
238
|
-
flatten =
|
239
|
-
lambda {|r|
|
240
|
-
(recurse = lambda {|v|
|
241
|
-
if v.is_a?(Hash) || v.is_a?(CouchRest::Document)
|
242
|
-
v.to_a.map{|v| recurse.call(v)}.flatten
|
243
|
-
elsif v.is_a?(Array)
|
244
|
-
v.flatten.map{|v| recurse.call(v)}
|
245
|
-
else
|
246
|
-
v.to_s
|
247
|
-
end
|
248
|
-
}).call(r)
|
249
|
-
}
|
250
|
-
self['couchrest-hash'] = Digest::MD5.hexdigest(flatten.call(base).sort.join(''))
|
251
|
-
end
|
252
|
-
|
253
|
-
def cache
|
254
|
-
Thread.current[:couchrest_design_cache] ||= {}
|
255
|
-
end
|
256
|
-
def cache_checksum(db)
|
257
|
-
cache[uri(db)]
|
258
|
-
end
|
259
|
-
def set_cache_checksum(db, checksum)
|
260
|
-
cache[uri(db)] = checksum
|
261
|
-
end
|
262
|
-
|
263
|
-
def apply_defaults
|
264
|
-
merge!(
|
265
|
-
"language" => "javascript",
|
266
|
-
"views" => { }
|
267
|
-
)
|
268
|
-
end
|
269
|
-
|
270
|
-
|
271
|
-
class << self
|
272
|
-
|
273
|
-
def method_name(prefix = nil)
|
274
|
-
(prefix ? "#{prefix}_" : '') + 'design_doc'
|
275
|
-
end
|
276
|
-
|
277
|
-
end
|
278
|
-
|
279
|
-
end
|
280
|
-
end
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
|
@@ -1,92 +0,0 @@
|
|
1
|
-
module CouchRest
|
2
|
-
module Model
|
3
|
-
|
4
|
-
# Handle CouchDB migrations.
|
5
|
-
#
|
6
|
-
# Actual migrations are handled by the Design document, this serves as a utility
|
7
|
-
# to find all the CouchRest Model submodels and perform the migration on them.
|
8
|
-
#
|
9
|
-
# Also contains some more advanced support for handling proxied models.
|
10
|
-
#
|
11
|
-
# Examples of usage:
|
12
|
-
#
|
13
|
-
# # Ensure all models have been loaded (only Rails)
|
14
|
-
# CouchRest::Model::Migrate.load_all_models
|
15
|
-
#
|
16
|
-
# # Migrate all regular models (not proxied)
|
17
|
-
# CouchRest::Model::Migrate.all_models
|
18
|
-
#
|
19
|
-
# # Migrate all models and submodels of proxies
|
20
|
-
# CouchRest::Model::Migrate.all_models_and_proxies
|
21
|
-
#
|
22
|
-
class Migrate
|
23
|
-
|
24
|
-
def self.all_models
|
25
|
-
callbacks = migrate_each_model(find_models)
|
26
|
-
cleanup(callbacks)
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.all_models_and_proxies
|
30
|
-
callbacks = migrate_each_model(find_models)
|
31
|
-
callbacks += migrate_each_proxying_model(find_proxying_models)
|
32
|
-
cleanup(callbacks)
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.load_all_models
|
36
|
-
# Make a reasonable effort to load all models
|
37
|
-
return unless defined?(Rails)
|
38
|
-
Dir[Rails.root + 'app/models/**/*.rb'].each do |path|
|
39
|
-
require path
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def self.find_models
|
44
|
-
CouchRest::Model::Base.subclasses.reject{|m| m.proxy_owner_method.present?}
|
45
|
-
end
|
46
|
-
|
47
|
-
def self.find_proxying_models
|
48
|
-
CouchRest::Model::Base.subclasses.reject{|m| m.proxy_database_method.blank?}
|
49
|
-
end
|
50
|
-
|
51
|
-
def self.migrate_each_model(models, db = nil)
|
52
|
-
callbacks = [ ]
|
53
|
-
models.each do |model|
|
54
|
-
model.design_docs.each do |design|
|
55
|
-
callbacks << migrate_design(model, design, db)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
callbacks
|
59
|
-
end
|
60
|
-
|
61
|
-
def self.migrate_each_proxying_model(models)
|
62
|
-
callbacks = [ ]
|
63
|
-
models.each do |model|
|
64
|
-
submodels = model.proxied_model_names.map{|n| n.constantize}
|
65
|
-
model.all.each do |base|
|
66
|
-
puts "Finding proxied models for #{model}: \"#{base.send(model.proxy_database_method)}\""
|
67
|
-
callbacks += migrate_each_model(submodels, base.proxy_database)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
callbacks
|
71
|
-
end
|
72
|
-
|
73
|
-
def self.migrate_design(model, design, db = nil)
|
74
|
-
print "Migrating #{model.to_s}##{design.method_name}... "
|
75
|
-
callback = design.migrate(db) do |result|
|
76
|
-
puts "#{result.to_s.gsub(/_/, ' ')}"
|
77
|
-
end
|
78
|
-
# Return the callback hash if there is one
|
79
|
-
callback ? {:design => design, :proc => callback, :db => db || model.database} : nil
|
80
|
-
end
|
81
|
-
|
82
|
-
def self.cleanup(methods)
|
83
|
-
methods.compact.each do |cb|
|
84
|
-
name = "/#{cb[:db].name}/#{cb[:design]['_id']}"
|
85
|
-
puts "Activating new design: #{name}"
|
86
|
-
cb[:proc].call
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|