couchrest_model 2.0.0.beta2 → 2.0.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.
- 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
|