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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +182 -0
  3. data/LICENSE.txt +15 -0
  4. data/README.md +294 -0
  5. data/lib/core_ext/date.rb +15 -0
  6. data/lib/core_ext/time.rb +23 -0
  7. data/lib/simply_couch/class_methods_base.rb +72 -0
  8. data/lib/simply_couch/has_attachment.rb +225 -0
  9. data/lib/simply_couch/include_relation.rb +160 -0
  10. data/lib/simply_couch/instance_methods.rb +356 -0
  11. data/lib/simply_couch/locale/en.yml +5 -0
  12. data/lib/simply_couch/model/ancestry.rb +307 -0
  13. data/lib/simply_couch/model/association_property.rb +26 -0
  14. data/lib/simply_couch/model/attachments.rb +90 -0
  15. data/lib/simply_couch/model/belongs_to.rb +140 -0
  16. data/lib/simply_couch/model/database.rb +209 -0
  17. data/lib/simply_couch/model/embedded_in.rb +196 -0
  18. data/lib/simply_couch/model/find_by.rb +202 -0
  19. data/lib/simply_couch/model/finders.rb +77 -0
  20. data/lib/simply_couch/model/has_and_belongs_to_many.rb +223 -0
  21. data/lib/simply_couch/model/has_many.rb +177 -0
  22. data/lib/simply_couch/model/has_many_embedded.rb +187 -0
  23. data/lib/simply_couch/model/has_one.rb +75 -0
  24. data/lib/simply_couch/model/pagination.rb +25 -0
  25. data/lib/simply_couch/model/pagination_options.rb +55 -0
  26. data/lib/simply_couch/model/persistence.rb +411 -0
  27. data/lib/simply_couch/model/properties.rb +11 -0
  28. data/lib/simply_couch/model/validations.rb +28 -0
  29. data/lib/simply_couch/model/view/base_view_spec.rb +115 -0
  30. data/lib/simply_couch/model/view/custom_view_spec.rb +49 -0
  31. data/lib/simply_couch/model/view/custom_views.rb +50 -0
  32. data/lib/simply_couch/model/view/lists.rb +25 -0
  33. data/lib/simply_couch/model/view/model_view_spec.rb +106 -0
  34. data/lib/simply_couch/model/view/properties_view_spec.rb +53 -0
  35. data/lib/simply_couch/model/view/raw_view_spec.rb +30 -0
  36. data/lib/simply_couch/model/view/view_query.rb +98 -0
  37. data/lib/simply_couch/model/view.rb +8 -0
  38. data/lib/simply_couch/model/views/array_property_view_spec.rb +26 -0
  39. data/lib/simply_couch/model/views/deleted_model_view_spec.rb +43 -0
  40. data/lib/simply_couch/model/views.rb +2 -0
  41. data/lib/simply_couch/model.rb +195 -0
  42. data/lib/simply_couch/rake.rb +23 -0
  43. data/lib/simply_couch/storage.rb +147 -0
  44. data/lib/simply_couch.rb +26 -0
  45. 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