simply_couch 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +182 -0
- data/LICENSE.txt +15 -0
- data/README.md +294 -0
- data/lib/core_ext/date.rb +15 -0
- data/lib/core_ext/time.rb +23 -0
- data/lib/simply_couch/class_methods_base.rb +72 -0
- data/lib/simply_couch/has_attachment.rb +225 -0
- data/lib/simply_couch/include_relation.rb +160 -0
- data/lib/simply_couch/instance_methods.rb +356 -0
- data/lib/simply_couch/locale/en.yml +5 -0
- data/lib/simply_couch/model/ancestry.rb +307 -0
- data/lib/simply_couch/model/association_property.rb +26 -0
- data/lib/simply_couch/model/attachments.rb +90 -0
- data/lib/simply_couch/model/belongs_to.rb +140 -0
- data/lib/simply_couch/model/database.rb +209 -0
- data/lib/simply_couch/model/embedded_in.rb +196 -0
- data/lib/simply_couch/model/find_by.rb +202 -0
- data/lib/simply_couch/model/finders.rb +77 -0
- data/lib/simply_couch/model/has_and_belongs_to_many.rb +223 -0
- data/lib/simply_couch/model/has_many.rb +177 -0
- data/lib/simply_couch/model/has_many_embedded.rb +187 -0
- data/lib/simply_couch/model/has_one.rb +75 -0
- data/lib/simply_couch/model/pagination.rb +25 -0
- data/lib/simply_couch/model/pagination_options.rb +55 -0
- data/lib/simply_couch/model/persistence.rb +411 -0
- data/lib/simply_couch/model/properties.rb +11 -0
- data/lib/simply_couch/model/validations.rb +28 -0
- data/lib/simply_couch/model/view/base_view_spec.rb +115 -0
- data/lib/simply_couch/model/view/custom_view_spec.rb +49 -0
- data/lib/simply_couch/model/view/custom_views.rb +50 -0
- data/lib/simply_couch/model/view/lists.rb +25 -0
- data/lib/simply_couch/model/view/model_view_spec.rb +106 -0
- data/lib/simply_couch/model/view/properties_view_spec.rb +53 -0
- data/lib/simply_couch/model/view/raw_view_spec.rb +30 -0
- data/lib/simply_couch/model/view/view_query.rb +98 -0
- data/lib/simply_couch/model/view.rb +8 -0
- data/lib/simply_couch/model/views/array_property_view_spec.rb +26 -0
- data/lib/simply_couch/model/views/deleted_model_view_spec.rb +43 -0
- data/lib/simply_couch/model/views.rb +2 -0
- data/lib/simply_couch/model.rb +195 -0
- data/lib/simply_couch/rake.rb +23 -0
- data/lib/simply_couch/storage.rb +147 -0
- data/lib/simply_couch.rb +26 -0
- metadata +144 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
module SimplyCouch
|
|
2
|
+
module Model
|
|
3
|
+
module FindBy
|
|
4
|
+
include PaginationOptions
|
|
5
|
+
def _define_find_by(name, *args)
|
|
6
|
+
raise_when_not_found = name.to_s.end_with?('!')
|
|
7
|
+
name = name.to_s.chop.to_sym if raise_when_not_found
|
|
8
|
+
keys = name.to_s.sub(/^find_by_/, "").split("_and_").map(&:to_sym)
|
|
9
|
+
|
|
10
|
+
# replace asociation assignments with their property values if possible
|
|
11
|
+
keys.each.with_index do |key, i|
|
|
12
|
+
if properties.find{|p| p.name.to_sym == key.to_sym}.is_a?(SimplyCouch::Model::BelongsTo::Property)
|
|
13
|
+
keys[i] = "#{keys[i]}_id"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
view_name = name.to_s.sub(/^find_/, "").to_sym
|
|
18
|
+
view_keys = keys.length == 1 ? keys.first : keys
|
|
19
|
+
without_deleted_view_name = "#{view_name}_withoutdeleted"
|
|
20
|
+
without_deleted_view_keys = keys + [:deleted_at]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
unless respond_to?(view_name)
|
|
24
|
+
puts "Warning: Defining view #{self.name}##{view_name} with keys #{view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
|
|
25
|
+
view(view_name, key: view_keys)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if !respond_to?(without_deleted_view_name) && soft_deleting_enabled?
|
|
29
|
+
puts "Warning: Defining view #{self.name}##{without_deleted_view_name} with keys #{without_deleted_view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
|
|
30
|
+
view(without_deleted_view_name, key: without_deleted_view_keys)
|
|
31
|
+
end
|
|
32
|
+
if raise_when_not_found
|
|
33
|
+
(class << self; self end).instance_eval do
|
|
34
|
+
define_method(:"#{name}!") do |*key_args|
|
|
35
|
+
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
|
|
36
|
+
options.assert_valid_keys(:with_deleted)
|
|
37
|
+
with_deleted = options.delete(:with_deleted)
|
|
38
|
+
|
|
39
|
+
raise ArgumentError, "Too many or too few arguments, require #{keys.inspect}" unless keys.size == key_args.size
|
|
40
|
+
|
|
41
|
+
key_args.map!{|a| a.is_a?(SimplyCouch::Model) ? a.id : a}
|
|
42
|
+
|
|
43
|
+
if soft_deleting_enabled? && !with_deleted
|
|
44
|
+
key_args = key_args + [nil] # deleted_at
|
|
45
|
+
result = database.view(send(without_deleted_view_name, key: (key_args.size == 1 ? key_args.first : key_args), limit: 1, include_docs: true)).first
|
|
46
|
+
else
|
|
47
|
+
result = database.view(send(view_name, key: (key_args.size == 1 ? key_args.first : key_args), limit: 1, include_docs: true)).first
|
|
48
|
+
end
|
|
49
|
+
raise SimplyCouch::RecordNotFound unless result
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
send(:"#{name}!", *args)
|
|
54
|
+
else
|
|
55
|
+
(class << self; self end).instance_eval do
|
|
56
|
+
define_method(name) do |*key_args|
|
|
57
|
+
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
|
|
58
|
+
options.assert_valid_keys(:with_deleted)
|
|
59
|
+
with_deleted = options.delete(:with_deleted)
|
|
60
|
+
|
|
61
|
+
raise ArgumentError, "Too many or too few arguments, require #{keys.inspect}" unless keys.size == key_args.size
|
|
62
|
+
|
|
63
|
+
key_args.map!{|a| a.is_a?(SimplyCouch::Model) ? a.id : a}
|
|
64
|
+
|
|
65
|
+
if soft_deleting_enabled? && !with_deleted
|
|
66
|
+
key_args = key_args + [nil] # deleted_at
|
|
67
|
+
database.view(send(without_deleted_view_name, key: (key_args.size == 1 ? key_args.first : key_args), limit: 1, include_docs: true)).first
|
|
68
|
+
else
|
|
69
|
+
database.view(send(view_name, key: (key_args.size == 1 ? key_args.first : key_args), limit: 1, include_docs: true)).first
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
send(name, *args)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def _define_find_all_by(name, *args)
|
|
78
|
+
raise_when_not_found = name.to_s.end_with?('!')
|
|
79
|
+
name = name.to_s.chop.to_sym if raise_when_not_found
|
|
80
|
+
keys = name.to_s.sub(/^find_all_by_/, "").split("_and_")
|
|
81
|
+
|
|
82
|
+
# replace asociation assignments with their property values if possible
|
|
83
|
+
keys.each.with_index do |key, i|
|
|
84
|
+
if properties.find{|p| p.name.to_sym == key.to_sym}.is_a?(SimplyCouch::Model::BelongsTo::Property)
|
|
85
|
+
keys[i] = "#{keys[i]}_id"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
view_name = name.to_s.sub(/^find_all_/, "").to_sym
|
|
90
|
+
count_name = name.to_s.sub(/^find_all_/, 'count_').to_sym
|
|
91
|
+
view_keys = keys.length == 1 ? keys.first : keys
|
|
92
|
+
without_deleted_view_name = "#{view_name}_withoutdeleted"
|
|
93
|
+
without_deleted_view_keys = keys + [:deleted_at]
|
|
94
|
+
|
|
95
|
+
unless respond_to?(view_name)
|
|
96
|
+
puts "Warning: Defining view #{self.name}##{view_name} with keys #{view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
|
|
97
|
+
view(view_name, key: view_keys)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if !respond_to?(without_deleted_view_name) && soft_deleting_enabled?
|
|
101
|
+
puts "Warning: Defining view #{self.name}##{without_deleted_view_name} with keys #{without_deleted_view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
|
|
102
|
+
view(without_deleted_view_name, key: without_deleted_view_keys)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if raise_when_not_found
|
|
106
|
+
(class << self; self end).instance_eval do
|
|
107
|
+
define_method(:"#{name}!") do |*key_args|
|
|
108
|
+
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
|
|
109
|
+
with_pagination_options(options.update(total_entries: send(count_name, *key_args))) do |options|
|
|
110
|
+
options.assert_valid_keys(:with_deleted, :limit, :skip, :keys)
|
|
111
|
+
with_deleted = options.delete(:with_deleted)
|
|
112
|
+
|
|
113
|
+
key_args.map!{|a| a.is_a?(SimplyCouch::Model) ? a.id : a}
|
|
114
|
+
options[:key] = key_args.first if key_args.size == 1
|
|
115
|
+
options[:key] = key_args if key_args.size > 1
|
|
116
|
+
options[:include_docs] = true
|
|
117
|
+
|
|
118
|
+
raise ArgumentError, "Too many or too few arguments, require #{keys.inspect}" unless keys.size == key_args.size || options[:keys]
|
|
119
|
+
|
|
120
|
+
key_args.map!{|a| a.is_a?(SimplyCouch::Model) ? a.id : a}
|
|
121
|
+
|
|
122
|
+
if soft_deleting_enabled? && !with_deleted
|
|
123
|
+
options[:key] = Array.wrap(options[:key]) + [nil] # deleted_at
|
|
124
|
+
result = database.view(send(without_deleted_view_name, options))
|
|
125
|
+
else
|
|
126
|
+
result = database.view(send(view_name, options))
|
|
127
|
+
end
|
|
128
|
+
raise SimplyCouch::RecordNotFound unless result && result.any?
|
|
129
|
+
result
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
send(:"#{name}!", *args)
|
|
134
|
+
else
|
|
135
|
+
(class << self; self end).instance_eval do
|
|
136
|
+
define_method(name) do |*key_args|
|
|
137
|
+
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
|
|
138
|
+
with_pagination_options(options.update(total_entries: send(count_name, *key_args))) do |options|
|
|
139
|
+
options.assert_valid_keys(:with_deleted, :limit, :skip, :keys)
|
|
140
|
+
with_deleted = options.delete(:with_deleted)
|
|
141
|
+
|
|
142
|
+
key_args.map!{|a| a.is_a?(SimplyCouch::Model) ? a.id : a}
|
|
143
|
+
options[:key] = key_args.first if key_args.size == 1
|
|
144
|
+
options[:key] = key_args if key_args.size > 1
|
|
145
|
+
options[:include_docs] = true
|
|
146
|
+
|
|
147
|
+
raise ArgumentError, "Too many or too few arguments, require #{keys.inspect}" unless keys.size == key_args.size || options[:keys]
|
|
148
|
+
|
|
149
|
+
if soft_deleting_enabled? && !with_deleted
|
|
150
|
+
options[:key] = Array.wrap(options[:key]) + [nil] # deleted_at
|
|
151
|
+
database.view(send(without_deleted_view_name, options))
|
|
152
|
+
else
|
|
153
|
+
database.view(send(view_name, options))
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
send(name, *args)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def _define_count_by(name, *args)
|
|
163
|
+
keys = name.to_s.sub(/^count_by_/, "").split("_and_")
|
|
164
|
+
view_name = name.to_s.sub(/^count_/, "").to_sym
|
|
165
|
+
view_keys = keys.length == 1 ? keys.first : keys
|
|
166
|
+
without_deleted_view_name = "#{view_name}_withoutdeleted"
|
|
167
|
+
without_deleted_view_keys = keys + [:deleted_at]
|
|
168
|
+
|
|
169
|
+
unless respond_to?(view_name)
|
|
170
|
+
puts "Warning: Defining view #{self.name}##{view_name} with keys #{view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
|
|
171
|
+
view(view_name, key: view_keys)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
if !respond_to?(without_deleted_view_name) && soft_deleting_enabled?
|
|
175
|
+
puts "Warning: Defining view #{self.name}##{without_deleted_view_name} with keys #{without_deleted_view_keys.inspect} at call time, please add it to the class body. (Called from #{caller[1]})"
|
|
176
|
+
view(without_deleted_view_name, key: without_deleted_view_keys)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
(class << self; self end).instance_eval do
|
|
180
|
+
define_method("#{name}") do |*key_args|
|
|
181
|
+
options = key_args.last.is_a?(Hash) ? key_args.pop : {}
|
|
182
|
+
options.assert_valid_keys(:with_deleted, :keys)
|
|
183
|
+
with_deleted = options.delete(:with_deleted)
|
|
184
|
+
options[:key] = key_args.first if key_args.size == 1
|
|
185
|
+
options[:key] = key_args if key_args.size > 1
|
|
186
|
+
options[:reduce] = true
|
|
187
|
+
|
|
188
|
+
if soft_deleting_enabled? && !with_deleted
|
|
189
|
+
options[:key] = Array.wrap(options[:key]) + [nil] # deleted_at
|
|
190
|
+
database.view(send(without_deleted_view_name, options))
|
|
191
|
+
else
|
|
192
|
+
database.view(send(view_name, options))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
send(name, *args)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
module SimplyCouch
|
|
2
|
+
module Model
|
|
3
|
+
module Finders
|
|
4
|
+
include PaginationOptions
|
|
5
|
+
def find(*args)
|
|
6
|
+
what = args.shift
|
|
7
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
|
8
|
+
if options && order = options.delete(:order)
|
|
9
|
+
options[:descending] = true if order == :desc
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
with_deleted = options.delete(:with_deleted)
|
|
13
|
+
|
|
14
|
+
result = case what
|
|
15
|
+
when :all
|
|
16
|
+
if options.has_key?(:page)
|
|
17
|
+
options[:total_entries] = count
|
|
18
|
+
end
|
|
19
|
+
if with_deleted || !soft_deleting_enabled?
|
|
20
|
+
with_pagination_options(options) do |options|
|
|
21
|
+
database.view(all_documents(options))
|
|
22
|
+
end
|
|
23
|
+
else
|
|
24
|
+
with_pagination_options(options) do |options|
|
|
25
|
+
database.view(all_documents_without_deleted(options.update(include_docs: true)))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
when :first
|
|
29
|
+
if with_deleted || !soft_deleting_enabled?
|
|
30
|
+
database.view(all_documents(options.update(limit: 1, include_docs: true))).first
|
|
31
|
+
else
|
|
32
|
+
database.view(all_documents_without_deleted(options.update(limit: 1, include_docs: true))).first
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
raise SimplyCouch::Error, "Can't load record without an id" if what.nil?
|
|
36
|
+
document = database.load_document(what)
|
|
37
|
+
if what.is_a?(Array) # Support for multiple find
|
|
38
|
+
#TODO: extended validation and checking, for array arguments
|
|
39
|
+
raise SimplyCouch::NotImplementedError
|
|
40
|
+
else
|
|
41
|
+
# TODO, this part should be better.
|
|
42
|
+
raise(SimplyCouch::RecordNotFound, "#{self.name} could not be found with #{what.inspect}") unless document.present?
|
|
43
|
+
raise(SimplyCouch::RecordNotFound, "#{self.name} could not be found with #{what.inspect} — got #{document.class.name}") unless document.is_a?(self)
|
|
44
|
+
if document.deleted? && !with_deleted
|
|
45
|
+
raise(SimplyCouch::RecordNotFound, "#{self.name} could not be found with #{what.inspect}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
document
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def all(*args)
|
|
53
|
+
find(:all, *args)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def first(*args)
|
|
57
|
+
find(:first, *args)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def last(*args)
|
|
61
|
+
options = args.last.is_a?(Hash) ? args.last : {}
|
|
62
|
+
find(:first, options.update(order: :desc))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def count(options = {})
|
|
66
|
+
options.assert_valid_keys(:with_deleted)
|
|
67
|
+
with_deleted = options[:with_deleted]
|
|
68
|
+
|
|
69
|
+
if with_deleted || !soft_deleting_enabled?
|
|
70
|
+
database.view(all_documents(reduce: true))
|
|
71
|
+
else
|
|
72
|
+
database.view(all_documents_without_deleted(reduce: true))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
module SimplyCouch
|
|
2
|
+
module Model
|
|
3
|
+
module HasAndBelongsToMany
|
|
4
|
+
def has_and_belongs_to_many(name, options = {})
|
|
5
|
+
check_existing_properties(name, SimplyCouch::Model::HasAndBelongsToMany::Property)
|
|
6
|
+
properties << SimplyCouch::Model::HasAndBelongsToMany::Property.new(self, name, options)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def define_has_and_belongs_to_many_property(foreign_key)
|
|
10
|
+
property foreign_key
|
|
11
|
+
|
|
12
|
+
# Only allow non nil or empty values to be set
|
|
13
|
+
define_method "#{foreign_key}=" do |value|
|
|
14
|
+
super(value.is_a?(Array) ? value.select{|v| v.present?} : value)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def define_has_and_belongs_to_many_views(name, options)
|
|
19
|
+
key_order = options[:class_storing_keys] == self.name ? "doc.#{options[:foreign_key]}[index], doc._id" : "doc._id, doc.#{options[:foreign_key]}[index]"
|
|
20
|
+
value = options[:class_storing_keys] == self.name ? 1 : "{ _id :doc.#{options[:foreign_key]}[index]}"
|
|
21
|
+
association_property = if name.to_s.index('__')
|
|
22
|
+
# Already defined properly
|
|
23
|
+
name
|
|
24
|
+
elsif options[:class_name].present?
|
|
25
|
+
# Determine namespace and replace last argument with given name
|
|
26
|
+
name_hierarchy = options[:class_name].to_s.underscore.split(/\/|::/)
|
|
27
|
+
name_hierarchy[-1] = name
|
|
28
|
+
name_hierarchy.join('__')
|
|
29
|
+
elsif rindex = foreign_property.to_s.rindex('__')
|
|
30
|
+
# Make name based on current namespace
|
|
31
|
+
"#{foreign_property[0...rindex]}__#{name}"
|
|
32
|
+
else
|
|
33
|
+
# Just return the good old name
|
|
34
|
+
name
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
map_definition_without_deleted = <<-eos
|
|
38
|
+
function(doc) {
|
|
39
|
+
if (doc['ruby_class'] == '#{options[:class_storing_keys]}' && doc['#{options[:foreign_key]}'] != null) {
|
|
40
|
+
if (doc['#{soft_delete_attribute}'] && doc['#{soft_delete_attribute}'] != null){
|
|
41
|
+
// "soft" deleted
|
|
42
|
+
}else{
|
|
43
|
+
for (var index in doc.#{options[:foreign_key]}) {
|
|
44
|
+
emit([#{key_order}], #{value});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
eos
|
|
50
|
+
|
|
51
|
+
reduce_definition = options[:class_storing_keys] == self.name ? "_sum" : <<-eos
|
|
52
|
+
function(key, values) {
|
|
53
|
+
var sum = 0;
|
|
54
|
+
for (var i in values){
|
|
55
|
+
if (typeof(i) == 'number'){
|
|
56
|
+
sum = sum + i;
|
|
57
|
+
} else {
|
|
58
|
+
sum = sum + 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return sum;
|
|
62
|
+
}
|
|
63
|
+
eos
|
|
64
|
+
|
|
65
|
+
view "association_#{foreign_property}_has_and_belongs_to_many_#{association_property}",
|
|
66
|
+
:map_function => map_definition_without_deleted,
|
|
67
|
+
:reduce_function => reduce_definition,
|
|
68
|
+
:type => :custom,
|
|
69
|
+
:include_docs => true
|
|
70
|
+
|
|
71
|
+
map_definition_with_deleted = <<-eos
|
|
72
|
+
function(doc) {
|
|
73
|
+
if (doc['ruby_class'] == '#{options[:class_storing_keys]}' && doc['#{options[:foreign_key]}'] != null) {
|
|
74
|
+
for (var index in doc.#{options[:foreign_key]}) {
|
|
75
|
+
emit([#{key_order}], #{value});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
eos
|
|
80
|
+
|
|
81
|
+
view "association_#{self.name.underscore.gsub('/', '__')}_has_and_belongs_to_many_#{name}_with_deleted",
|
|
82
|
+
:map_function => map_definition_with_deleted,
|
|
83
|
+
:reduce_function => reduce_definition,
|
|
84
|
+
:type => :custom,
|
|
85
|
+
:include_docs => true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def define_has_and_belongs_to_many_getter(name, options)
|
|
89
|
+
define_method(name) do |*args|
|
|
90
|
+
local_options = args.first && args.first.is_a?(Hash) && args.first
|
|
91
|
+
forced_reload, with_deleted, limit, descending, skip = extract_association_options(local_options)
|
|
92
|
+
|
|
93
|
+
cached_results = send("_get_cached_#{name}")
|
|
94
|
+
cache_key = _cache_key_for(local_options)
|
|
95
|
+
return cached_results[cache_key] || [] unless persisted?
|
|
96
|
+
if forced_reload || cached_results[cache_key].nil?
|
|
97
|
+
cached_results[cache_key] = find_associated_via_join_view(options[:class_name], self.class, :with_deleted => with_deleted, :limit => limit, :descending => descending, :foreign_key => options[:foreign_key], :skip => skip)
|
|
98
|
+
instance_variable_set("@#{name}", cached_results)
|
|
99
|
+
end
|
|
100
|
+
cached_results[cache_key]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def define_has_and_belongs_to_many_setter_add(name, options)
|
|
105
|
+
define_method("add_#{name.to_s.singularize}") do |value|
|
|
106
|
+
klass = self.class.get_class_from_name(name)
|
|
107
|
+
raise ArgumentError, "expected #{klass} got #{value.class}" unless value.is_a?(klass)
|
|
108
|
+
|
|
109
|
+
if options[:class_storing_keys] == self.class.name
|
|
110
|
+
self.send("#{options[:foreign_key]}=", ((send(options[:foreign_key]) || []) + [value.id]).uniq )
|
|
111
|
+
self.save(false)
|
|
112
|
+
else
|
|
113
|
+
value.send("#{options[:foreign_key]}=", ((value.send(options[:foreign_key]) || []) + [self.id]).uniq )
|
|
114
|
+
value.save(false)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
cached_results = send("_get_cached_#{name}")[:all]
|
|
118
|
+
send("_set_cached_#{name}", (cached_results || []) << value, :all)
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def define_has_and_belongs_to_many_setter_remove(name, options)
|
|
124
|
+
define_method "remove_#{name.to_s.singularize}" do |value|
|
|
125
|
+
klass = self.class.get_class_from_name(name)
|
|
126
|
+
raise ArgumentError, "expected #{klass} got #{value.class}" unless value.is_a?(klass)
|
|
127
|
+
|
|
128
|
+
if options[:class_storing_keys] == self.class.name
|
|
129
|
+
raise ArgumentError, "cannot remove not mine" unless (send(options[:foreign_key]) || []).include?(value.id)
|
|
130
|
+
else
|
|
131
|
+
raise ArgumentError, "cannot remove not mine" unless (value.send(options[:foreign_key]) || []).include?(id)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if options[:class_storing_keys] == self.class.name
|
|
135
|
+
foreign_keys = (send(options[:foreign_key]) || []) - [value.id]
|
|
136
|
+
send("#{options[:foreign_key]}=", foreign_keys)
|
|
137
|
+
save(false)
|
|
138
|
+
else
|
|
139
|
+
foreign_keys = (value.send(options[:foreign_key]) || []) - [self.id]
|
|
140
|
+
value.send("#{options[:foreign_key]}=", foreign_keys)
|
|
141
|
+
value.save(false)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
cached_results = send("_get_cached_#{name}")[:all]
|
|
145
|
+
send("_set_cached_#{name}", (cached_results || []).delete_if{|item| item.id == value.id}, :all)
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def define_has_and_belongs_to_many_setter_remove_all(name, options)
|
|
151
|
+
define_method "remove_all_#{name}" do
|
|
152
|
+
all = send("#{name}", :force_reload => true)
|
|
153
|
+
|
|
154
|
+
all.collect{|i| i}.each do |item|
|
|
155
|
+
send("remove_#{name.to_s.singularize}", item)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def define_has_and_belongs_to_many_count(name, options, through = nil)
|
|
161
|
+
method_name = name.to_s.singularize.underscore.gsub('/', '__') + "_count"
|
|
162
|
+
define_method(method_name) do |*args|
|
|
163
|
+
local_options = args.first && args.first.is_a?(Hash) && args.first
|
|
164
|
+
forced_reload, with_deleted, limit, descending = extract_association_options(local_options)
|
|
165
|
+
|
|
166
|
+
if forced_reload || instance_variable_get("@#{method_name}").nil?
|
|
167
|
+
instance_variable_set("@#{method_name}", count_associated_via_join_view(through || options[:class_name], self.class, :with_deleted => with_deleted, :foreign_key => options[:foreign_key]))
|
|
168
|
+
end
|
|
169
|
+
instance_variable_get("@#{method_name}")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def define_has_and_belongs_to_many_after_destroy_cleanup(name, options)
|
|
174
|
+
if options[:class_storing_keys] == self.name
|
|
175
|
+
define_method "has_and_belongs_to_many_clean_up_after_destroy" do |property|
|
|
176
|
+
nil # deleting is enough as we store the keys
|
|
177
|
+
end
|
|
178
|
+
else
|
|
179
|
+
define_method "has_and_belongs_to_many_clean_up_after_destroy" do |property|
|
|
180
|
+
send("remove_all_#{property.name}")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
class Property < SimplyCouch::Model::AssociationProperty
|
|
186
|
+
|
|
187
|
+
def initialize(owner_clazz, name, options = {})
|
|
188
|
+
options = {
|
|
189
|
+
:storing_keys => false,
|
|
190
|
+
:class_name => owner_clazz.find_association_class_name(name),
|
|
191
|
+
:foreign_key => nil,
|
|
192
|
+
}.update(options)
|
|
193
|
+
|
|
194
|
+
# there is only one pair of foreign_keys and it usualy the name of the class not storing the keys
|
|
195
|
+
if options[:foreign_key].blank?
|
|
196
|
+
if options[:storing_keys]
|
|
197
|
+
options[:foreign_key] = options[:class_name].singularize.underscore.sub(/.*\//, '').foreign_key.pluralize.to_sym
|
|
198
|
+
else
|
|
199
|
+
options[:foreign_key] = owner_clazz.name.singularize.underscore.sub(/.*\//, '').foreign_key.pluralize.to_sym
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
options[:class_storing_keys] = options[:storing_keys] ? owner_clazz.name : options[:class_name]
|
|
203
|
+
@name, @options = name, options
|
|
204
|
+
|
|
205
|
+
options.assert_valid_keys(:class_name, :foreign_key, :storing_keys, :class_storing_keys)
|
|
206
|
+
|
|
207
|
+
owner_clazz.class_eval do
|
|
208
|
+
_define_cache_accessors(name, options)
|
|
209
|
+
define_has_and_belongs_to_many_property(options[:foreign_key]) if options[:storing_keys]
|
|
210
|
+
define_has_and_belongs_to_many_views(name, options)
|
|
211
|
+
define_has_and_belongs_to_many_getter(name, options)
|
|
212
|
+
define_has_and_belongs_to_many_setter_add(name, options)
|
|
213
|
+
define_has_and_belongs_to_many_setter_remove(name, options)
|
|
214
|
+
define_has_and_belongs_to_many_setter_remove_all(name, options)
|
|
215
|
+
define_has_and_belongs_to_many_count(name, options)
|
|
216
|
+
define_has_and_belongs_to_many_after_destroy_cleanup(name, options)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
module SimplyCouch
|
|
2
|
+
module Model
|
|
3
|
+
module HasMany
|
|
4
|
+
def has_many(name, options = {})
|
|
5
|
+
check_existing_properties(name, SimplyCouch::Model::HasMany::Property)
|
|
6
|
+
properties << SimplyCouch::Model::HasMany::Property.new(self, name, options)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def define_has_many_getter(name, options)
|
|
10
|
+
define_method(name) do |*args|
|
|
11
|
+
local_options = args.first && args.first.is_a?(Hash) && args.first
|
|
12
|
+
forced_reload, with_deleted, limit, descending = extract_association_options(local_options)
|
|
13
|
+
|
|
14
|
+
cached_results = send("_get_cached_#{name}")
|
|
15
|
+
cache_key = _cache_key_for(local_options)
|
|
16
|
+
if forced_reload || cached_results[cache_key].nil?
|
|
17
|
+
cached_results[cache_key] = find_associated(options[:class_name], self.class, :with_deleted => with_deleted, :limit => limit, :descending => descending, :foreign_key => options[:foreign_key])
|
|
18
|
+
instance_variable_set("@#{name}", cached_results)
|
|
19
|
+
self.class.set_parent_has_many_association_object(self, cached_results[cache_key])
|
|
20
|
+
end
|
|
21
|
+
cached_results[cache_key]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def define_has_many_through_getter(name, options, through)
|
|
26
|
+
raise ArgumentError, "no such relation: #{self} - #{through}" unless instance_methods.map(&:to_sym).include?(through.to_sym)
|
|
27
|
+
|
|
28
|
+
define_method(name) do |*args|
|
|
29
|
+
local_options = args.first && args.first.is_a?(Hash) && args.first
|
|
30
|
+
if local_options
|
|
31
|
+
local_options.assert_valid_keys(:force_reload, :with_deleted, :limit)
|
|
32
|
+
forced_reload = local_options[:force_reload]
|
|
33
|
+
with_deleted = local_options[:with_deleted]
|
|
34
|
+
limit = local_options[:limit]
|
|
35
|
+
else
|
|
36
|
+
forced_reload = false
|
|
37
|
+
with_deleted = false
|
|
38
|
+
limit = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
cached_results = send("_get_cached_#{name}")
|
|
42
|
+
cache_key = _cache_key_for(local_options)
|
|
43
|
+
|
|
44
|
+
if forced_reload || cached_results[cache_key].nil?
|
|
45
|
+
|
|
46
|
+
# there is probably a faster way to query this
|
|
47
|
+
through_property = self.class.properties.find{|p| p.name == through}
|
|
48
|
+
if through_property && through_property.respond_to?(:options) && through_property.options[:class_name].present?
|
|
49
|
+
through_finder = through_property.options[:class_name].constantize.foreign_property
|
|
50
|
+
else
|
|
51
|
+
through_finder = through # try with the association name
|
|
52
|
+
end
|
|
53
|
+
intermediate_objects = find_associated(through_finder, self.class, :with_deleted => with_deleted, :limit => limit, :foreign_key => options[:foreign_key])
|
|
54
|
+
|
|
55
|
+
through_objects = intermediate_objects.map do |intermediate_object|
|
|
56
|
+
intermediate_object.send(name.to_s.singularize.underscore.gsub('/', '__'), :with_deleted => with_deleted)
|
|
57
|
+
end.flatten.uniq
|
|
58
|
+
cached_results[cache_key] = through_objects
|
|
59
|
+
instance_variable_set("@#{name}", cached_results)
|
|
60
|
+
end
|
|
61
|
+
cached_results[cache_key]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def define_has_many_setter_add(name, options)
|
|
66
|
+
define_method("add_#{name.to_s.singularize}") do |value|
|
|
67
|
+
klass = self.class.get_class_from_name(name)
|
|
68
|
+
raise ArgumentError, "expected #{klass} got #{value.class}" unless value.is_a?(klass)
|
|
69
|
+
foreign_key = "#{self.class.foreign_key}="
|
|
70
|
+
# If foreign key is namespace: admin__user, try jus user if admin_user_id is not present
|
|
71
|
+
foreign_key.gsub!(/.*__/, '') if !value.respond_to?(foreign_key) && value.respond_to?(foreign_key.sub(/.*__/, ''))
|
|
72
|
+
value.send(foreign_key, id)
|
|
73
|
+
value.save(false)
|
|
74
|
+
|
|
75
|
+
cached_results = send("_get_cached_#{name}")[:all]
|
|
76
|
+
send("_set_cached_#{name}", (cached_results || []) << value, :all)
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def define_has_many_setter_remove(name, options)
|
|
82
|
+
define_method "remove_#{name.to_s.singularize}" do |value|
|
|
83
|
+
klass = self.class.get_class_from_name(name)
|
|
84
|
+
raise ArgumentError, "expected #{klass} got #{value.class}" unless value.is_a?(klass)
|
|
85
|
+
raise ArgumentError, "cannot remove not mine" unless value.send(self.class.foreign_key.to_sym) == id
|
|
86
|
+
|
|
87
|
+
if options[:dependent] == :destroy
|
|
88
|
+
value.destroy
|
|
89
|
+
elsif options[:dependent] == :ignore
|
|
90
|
+
# skip
|
|
91
|
+
else # nullify
|
|
92
|
+
foreign_key = "#{self.class.foreign_key}="
|
|
93
|
+
# If foreign key is namespace: admin__user, try jus user if admin_user_id is not present
|
|
94
|
+
foreign_key.gsub!(/.*__/, '') if !value.respond_to?(foreign_key) && value.respond_to?(foreign_key.sub(/.*__/, ''))
|
|
95
|
+
value.send(foreign_key, nil)
|
|
96
|
+
value.save(false)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
cached_results = send("_get_cached_#{name}")[:all]
|
|
100
|
+
send("_set_cached_#{name}", (cached_results || []).delete_if{|item| item.id == value.id}, :all)
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def define_has_many_setter_remove_all(name, options)
|
|
106
|
+
define_method "remove_all_#{name}" do
|
|
107
|
+
all = send("#{name}", :force_reload => true)
|
|
108
|
+
|
|
109
|
+
all.collect{|i| i}.each do |item|
|
|
110
|
+
send("remove_#{name.to_s.singularize}", item)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def define_has_many_count(name, options, through = nil)
|
|
116
|
+
method_name = name.to_s.singularize.underscore.gsub('/', '__') + "_count"
|
|
117
|
+
define_method(method_name) do |*args|
|
|
118
|
+
local_options = args.first && args.first.is_a?(Hash) && args.first
|
|
119
|
+
if local_options
|
|
120
|
+
local_options.assert_valid_keys(:force_reload, :with_deleted)
|
|
121
|
+
forced_reload = local_options[:force_reload]
|
|
122
|
+
with_deleted = local_options[:with_deleted]
|
|
123
|
+
else
|
|
124
|
+
forced_reload = false
|
|
125
|
+
with_deleted = false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if forced_reload || instance_variable_get("@#{method_name}").nil?
|
|
129
|
+
instance_variable_set("@#{method_name}", count_associated(through || options[:class_name], self.class, :with_deleted => with_deleted, :foreign_key => options[:foreign_key]))
|
|
130
|
+
end
|
|
131
|
+
instance_variable_get("@#{method_name}")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def set_parent_has_many_association_object(parent, child_collection)
|
|
136
|
+
child_collection.each do |child|
|
|
137
|
+
if child.respond_to?("#{parent.class.name.to_s.singularize.downcase}=")
|
|
138
|
+
child.send("#{parent.class.name.to_s.singularize.camelize.downcase}=", parent)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class Property < SimplyCouch::Model::AssociationProperty
|
|
144
|
+
|
|
145
|
+
def initialize(owner_clazz, name, options = {})
|
|
146
|
+
options = {
|
|
147
|
+
:dependent => :nullify,
|
|
148
|
+
:through => nil,
|
|
149
|
+
:class_name => owner_clazz.find_association_class_name(name),
|
|
150
|
+
:foreign_key => nil
|
|
151
|
+
}.update(options)
|
|
152
|
+
@name, @options = name, options
|
|
153
|
+
|
|
154
|
+
options.assert_valid_keys(:dependent, :through, :class_name, :foreign_key)
|
|
155
|
+
|
|
156
|
+
if options[:through]
|
|
157
|
+
owner_clazz.class_eval do
|
|
158
|
+
_define_cache_accessors(name, options)
|
|
159
|
+
define_has_many_through_getter(name, options, options[:through])
|
|
160
|
+
define_has_many_count(name, options, options[:through])
|
|
161
|
+
end
|
|
162
|
+
else
|
|
163
|
+
owner_clazz.class_eval do
|
|
164
|
+
_define_cache_accessors(name, options)
|
|
165
|
+
define_has_many_getter(name, options)
|
|
166
|
+
define_has_many_setter_add(name, options)
|
|
167
|
+
define_has_many_setter_remove(name, options)
|
|
168
|
+
define_has_many_setter_remove_all(name, options)
|
|
169
|
+
define_has_many_count(name, options)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|