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,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CouchDB inline attachment support.
4
+ # Adds put_attachment, fetch_attachment, delete_attachment, and attachment_names
5
+ # to any SimplyCouch model.
6
+ #
7
+ # Usage:
8
+ # class Invoice
9
+ # include SimplyCouch::Model
10
+ # include SimplyCouch::Model::Attachments
11
+ # end
12
+ #
13
+ # invoice.put_attachment('invoice.pdf', file, content_type: 'application/pdf')
14
+ # invoice.fetch_attachment('invoice.pdf') # => file data
15
+ # invoice.delete_attachment('invoice.pdf')
16
+ # invoice.attachment_names # => ['invoice.pdf', 'logo.png']
17
+ #
18
+ module SimplyCouch
19
+ module Model
20
+ module Attachments
21
+ def self.included(base)
22
+ base.after_save :_save_pending_attachments
23
+ end
24
+
25
+ # Upload a file as a CouchDB inline attachment on this document.
26
+ # The attachment is stored immediately — no need to call save separately.
27
+ # Returns the CouchDB result hash.
28
+ def put_attachment(name, file, content_type: 'binary/octet-stream')
29
+ result = _couchrest_database.put_attachment(
30
+ to_hash, name, file, content_type: content_type
31
+ )
32
+ self._rev = result['rev'] if result['ok']
33
+ result
34
+ end
35
+
36
+ # Fetch an attachment's binary data from CouchDB.
37
+ # Returns nil if the attachment doesn't exist.
38
+ def fetch_attachment(name)
39
+ _couchrest_database.fetch_attachment(to_hash, name)
40
+ rescue RestClient::ResourceNotFound
41
+ nil
42
+ end
43
+
44
+ # Queue an attachment for upload on next save.
45
+ # Useful when building a new document that hasn't been saved yet
46
+ # (no _rev available for immediate put_attachment).
47
+ def add_attachment(name, file, content_type: 'binary/octet-stream')
48
+ @_pending_attachments ||= {}
49
+ @_pending_attachments[name] = { file: file, content_type: content_type }
50
+ end
51
+
52
+ # Delete an attachment from this document.
53
+ # The attachment is removed immediately — no need to call save separately.
54
+ def delete_attachment(name)
55
+ result = _couchrest_database.delete_attachment(to_hash, name)
56
+ self._rev = result['rev'] if result['ok']
57
+ result
58
+ end
59
+
60
+ # List all attachment names on this document.
61
+ def attachment_names
62
+ (_attachments || {}).keys
63
+ end
64
+
65
+ # Check if an attachment exists.
66
+ def attachment?(name)
67
+ attachment_names.include?(name.to_s)
68
+ end
69
+
70
+ private
71
+
72
+ def _save_pending_attachments
73
+ return unless @_pending_attachments&.any?
74
+
75
+ @_pending_attachments.each do |name, opts|
76
+ put_attachment(name, opts[:file], content_type: opts[:content_type])
77
+ end
78
+ @_pending_attachments = nil
79
+ end
80
+
81
+ def _couchrest_database
82
+ if respond_to?(:database) && database.respond_to?(:couchrest_database)
83
+ database.couchrest_database
84
+ else
85
+ self.class.database.couchrest_database
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,140 @@
1
+ # Gracefully taken from CouchPotato after it has been removed for good.
2
+ module SimplyCouch
3
+ module Model
4
+ module BelongsTo
5
+
6
+ def belongs_to(name, options = {})
7
+ check_existing_properties(name, SimplyCouch::Model::BelongsTo::Property)
8
+ association_property = if name.to_s.index('__')
9
+ # Already defined properly
10
+ name
11
+ elsif options[:class_name].present?
12
+ # Determine namespace and replace last argument with given name
13
+ name_hierarchy = options[:class_name].to_s.underscore.split(/\/|::/)
14
+ name_hierarchy[-1] = name
15
+ name_hierarchy.join('__')
16
+ elsif rindex = foreign_property.to_s.rindex('__')
17
+ # Make name based on current namespace
18
+ "#{foreign_property[0...rindex]}__#{name}"
19
+ else
20
+ # Just return the good old name
21
+ name
22
+ end
23
+
24
+ map_definition_without_deleted = <<-eos
25
+ function(doc) {
26
+ if (doc['ruby_class'] == '#{self.to_s}' && doc['#{name}_id'] != null) {
27
+ if (doc['#{soft_delete_attribute}'] && doc['#{soft_delete_attribute}'] != null){
28
+ // "soft" deleted
29
+ }else{
30
+ emit([doc.#{name}_id, doc.created_at], 1);
31
+ }
32
+ }
33
+ }
34
+ eos
35
+
36
+ reduce_definition = "_sum"
37
+ view "association_#{foreign_property}_belongs_to_#{association_property}",
38
+ :map_function => map_definition_without_deleted,
39
+ :reduce_function => reduce_definition,
40
+ :type => :custom,
41
+ :include_docs => true
42
+
43
+ map_definition_with_deleted = <<-eos
44
+ function(doc) {
45
+ if (doc['ruby_class'] == '#{self.to_s}' && doc['#{name}_id'] != null) {
46
+ emit([doc.#{name}_id, doc.created_at], 1);
47
+ }
48
+ }
49
+ eos
50
+
51
+ view "association_#{foreign_property}_belongs_to_#{association_property}_with_deleted",
52
+ :map_function => map_definition_with_deleted,
53
+ :reduce_function => reduce_definition,
54
+ :type => :custom,
55
+ :include_docs => true
56
+
57
+ properties << SimplyCouch::Model::BelongsTo::Property.new(self, name, options)
58
+ end
59
+
60
+ class Property #:nodoc:
61
+ attr_accessor :name, :options
62
+
63
+ def initialize(owner_clazz, name, options = {})
64
+ @name = name
65
+ @options = {
66
+ :class_name => owner_clazz.find_association_class_name(name)
67
+ }.update(options)
68
+
69
+ @options.assert_valid_keys(:class_name)
70
+
71
+ owner_clazz.class_eval do
72
+ property :"#{name}_id"
73
+ alias_method :"#{name}_changed?", :"#{name}_id_changed?"
74
+
75
+ define_method name do |*args|
76
+ local_options = args.last.is_a?(Hash) ? args.last : {}
77
+ local_options.assert_valid_keys(:force_reload, :with_deleted)
78
+ forced_reload = local_options[:force_reload] || false
79
+ with_deleted = local_options[:with_deleted] || false
80
+
81
+ return instance_variable_get("@#{name}") unless instance_variable_get("@#{name}").nil? or forced_reload
82
+
83
+ if send("#{name}_id").present?
84
+ # Try to fetch the object. Does not have to be present. When relation dependency is ignore, the id remains.
85
+ # This will result in a id to a non existent object. Therefore the rescue for object
86
+ object = self.class.get_class_from_name(name).find(send("#{name}_id"), :with_deleted => with_deleted) rescue nil
87
+ instance_variable_set("@#{name}", object)
88
+ else
89
+ instance_variable_set("@#{name}", nil)
90
+ end
91
+ end
92
+
93
+ define_method "#{name}=" do |value|
94
+ klass = self.class.get_class_from_name(name)
95
+ raise ArgumentError, "expected #{klass} got #{value.class}" unless value.nil? || value.is_a?(klass)
96
+
97
+ if value
98
+ # Has many object update
99
+ value_has_many_name = klass.properties.find{|p| p.is_a?(SimplyCouch::Model::HasMany::Property) && p.options[:class_name] == self.class.name}.try(:name)
100
+ value.send(value_has_many_name) << self unless !value_has_many_name || value.send(value_has_many_name).include?(self)
101
+
102
+ # Has one object update
103
+ value_has_one_name = klass.properties.find{|p| p.is_a?(SimplyCouch::Model::HasOne::Property) && p.options[:class_name] == self.class.name}.try(:name)
104
+ value.instance_variable_set("@#{value_has_one_name}", self) unless !value_has_one_name || value.send(value_has_one_name) == self
105
+ end
106
+
107
+ # Mark changed if appropriate
108
+ # send("#{name}_will_change!") if value != instance_variable_get("@#{name}") is not a persisted property, do not mark as changed
109
+
110
+ instance_variable_set("@#{name}", value)
111
+ if value.nil?
112
+ send("#{name}_id=", nil)
113
+ else
114
+ send("#{name}_id=", value.id)
115
+ end
116
+ end
117
+
118
+ define_method "#{name}_id=" do |new_foreign_id|
119
+ super(new_foreign_id)
120
+ value = instance_variable_get("@#{name}")
121
+ remove_instance_variable("@#{name}") if instance_variable_defined?("@#{name}") && new_foreign_id != value.try(:id)
122
+ end
123
+ end
124
+ end
125
+ def build(object, json)
126
+ object.send "#{name}_id=", json["#{name}_id"]
127
+ end
128
+
129
+ def serialize(json, object)
130
+ json["#{name}_id"] = object.send("#{name}_id") if object.send("#{name}_id")
131
+ end
132
+ alias :value :serialize
133
+
134
+ def association?
135
+ true
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,209 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module Database
4
+ def database
5
+ @_simply_couch_database ||= DatabaseInstance.new(full_database_url)
6
+ end
7
+
8
+ # Override this to provide a custom database URL.
9
+ # In Rails, reads config/couchdb.yml automatically.
10
+ def couchrest_database_url
11
+ @_couchrest_database_url || detect_couchdb_url || ENV['COUCHDB_URL'] || 'http://127.0.0.1:5984'
12
+ end
13
+
14
+ def couchrest_database_url=(url)
15
+ @_couchrest_database_url = url
16
+ end
17
+
18
+ private
19
+
20
+ def full_database_url
21
+ base = couchrest_database_url
22
+ name = database_name
23
+ # Only append db name if URL doesn't already include it.
24
+ # Skip URL scheme slashes (http://) when checking for existing db name.
25
+ path_part = base.sub(%r{^https?://}, '')
26
+ if path_part.include?('/')
27
+ base # URL already has a database name
28
+ else
29
+ "#{base}/#{name}"
30
+ end
31
+ end
32
+
33
+ def database_name
34
+ @_database_name || detect_database_name || 'mozo_development'
35
+ end
36
+
37
+ def database_name=(name)
38
+ @_database_name = name
39
+ end
40
+
41
+ def detect_couchdb_url
42
+ return unless defined?(Rails) && Rails.root
43
+ config_path = Rails.root.join('config/couchdb.yml')
44
+ return unless File.exist?(config_path)
45
+ config = YAML.safe_load(ERB.new(File.read(config_path)).result, permitted_classes: [Symbol])
46
+ env_config = config[Rails.env] || config['development']
47
+ db_url = env_config['database'] if env_config.is_a?(Hash)
48
+ # Extract host:port from full URL like http://admin:pass@host:port/dbname
49
+ # Strip database name from full URL, skipping URL scheme
50
+ db_url&.sub(%r{/([^/]+)$}, '') { $1 if $1.include?('.') || $1.length < 15 }
51
+ end
52
+
53
+ def detect_database_name
54
+ return unless defined?(Rails) && Rails.root
55
+ config_path = Rails.root.join('config/couchdb.yml')
56
+ return unless File.exist?(config_path)
57
+ config = YAML.safe_load(ERB.new(File.read(config_path)).result, permitted_classes: [Symbol])
58
+ env_config = config[Rails.env] || config['development']
59
+ db_url = env_config['database'] if env_config.is_a?(Hash)
60
+ return unless db_url
61
+ URI.parse(db_url).path&.sub('/', '')
62
+ end
63
+ end
64
+
65
+ # Renamed from CompatibilityNote — this is the actual Database class
66
+ # (separate from the Database module above to avoid naming conflict)
67
+ class DatabaseInstance
68
+ attr_reader :couchrest_database
69
+
70
+ def initialize(couchrest_database)
71
+ if couchrest_database.is_a?(String)
72
+ # URL string — create CouchRest database
73
+ @couchrest_database = CouchRest.database(couchrest_database)
74
+ elsif couchrest_database.nil?
75
+ @couchrest_database = CouchRest.database('http://127.0.0.1:5984')
76
+ else
77
+ @couchrest_database = couchrest_database
78
+ end
79
+ end
80
+
81
+ def view(spec)
82
+ results = View::ViewQuery.new(
83
+ couchrest_database,
84
+ spec.design_document,
85
+ {spec.view_name => {
86
+ :map => spec.map_function,
87
+ :reduce => spec.reduce_function
88
+ }},
89
+ (spec.list_name ? {spec.list_name => spec.list_function} : nil),
90
+ spec.lib,
91
+ spec.language
92
+ ).query_view!(spec.view_parameters)
93
+ processed_results = spec.process_results results
94
+ processed_results.each do |document|
95
+ document.database = self if document.respond_to?(:database=)
96
+ end if processed_results.respond_to?(:each)
97
+ processed_results
98
+ end
99
+
100
+ def first(spec)
101
+ spec.view_parameters = spec.view_parameters.merge({:limit => 1})
102
+ view(spec).first
103
+ end
104
+
105
+ def save_document(document, validate = true)
106
+ begin
107
+ if document.new?
108
+ create_document(document, validate)
109
+ else
110
+ update_document(document, validate)
111
+ end
112
+ rescue CouchRest::Conflict
113
+ raise SimplyCouch::Conflict.new
114
+ end
115
+ end
116
+
117
+ def save_document!(document)
118
+ save_document(document) || raise("Validations failed: #{document.errors.full_messages}")
119
+ end
120
+
121
+ def load_document(id)
122
+ raise "Can't load a document without an id (got nil)" if id.nil?
123
+ instance = couchrest_database.get(id)
124
+ instance.database = self if instance.respond_to?(:database=)
125
+ instance
126
+ end
127
+
128
+ def destroy_document(document, run_callbacks = true)
129
+ if run_callbacks
130
+ document.run_callbacks :destroy do
131
+ document._deleted = true
132
+ couchrest_database.delete_doc document.to_hash
133
+ end
134
+ else
135
+ document._deleted = true
136
+ couchrest_database.delete_doc document.to_hash
137
+ end
138
+ document._id = nil
139
+ document._rev = nil
140
+ end
141
+
142
+ def bulk_load(ids)
143
+ response = couchrest_database.bulk_load ids
144
+ docs = response['rows'].map{|row| row["doc"]}.compact
145
+ docs.each{|doc| doc.database = self if doc.respond_to?(:database=) }
146
+ docs
147
+ end
148
+
149
+ def delete_document(document)
150
+ couchrest_database.delete_doc document.to_hash
151
+ end
152
+
153
+ private
154
+
155
+ def create_document(document, validate)
156
+ document.database = self
157
+ if validate
158
+ document.errors.clear
159
+ return false if false == document.run_callbacks(:validation) do
160
+ return false if false == document.run_callbacks(:validation_on_create) do
161
+ return false unless valid_document?(document)
162
+ end
163
+ end
164
+ end
165
+ return false if false == document.run_callbacks(:save) do
166
+ return false if false == document.run_callbacks(:create) do
167
+ res = couchrest_database.save_doc document.to_hash
168
+ document._rev = res['rev']
169
+ document._id = res['id']
170
+ end
171
+ end
172
+ true
173
+ end
174
+
175
+ def update_document(document, validate)
176
+ if validate
177
+ document.errors.clear
178
+ return false if false == document.run_callbacks(:validation) do
179
+ return false if false == document.run_callbacks(:validation_on_update) do
180
+ return false unless valid_document?(document)
181
+ end
182
+ end
183
+ end
184
+ return false if false == document.run_callbacks(:save) do
185
+ return false if false == document.run_callbacks(:update) do
186
+ if document.changed?
187
+ res = couchrest_database.save_doc document.to_hash
188
+ document._rev = res['rev']
189
+ end
190
+ end
191
+ end
192
+ true
193
+ end
194
+
195
+ def valid_document?(document)
196
+ original_errors_hash = document.errors.to_hash
197
+ document.valid?
198
+ original_errors_hash.each do |k, v|
199
+ if v.respond_to?(:each)
200
+ v.each {|message| document.errors.add(k, message)}
201
+ else
202
+ document.errors.add(k, v)
203
+ end
204
+ end
205
+ document.errors.empty?
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,196 @@
1
+ # Gracefully taken from CouchPotato after it has been removed for good.
2
+ module SimplyCouch
3
+ module Model
4
+ module EmbeddedIn
5
+
6
+ def is_embedded_in(name, options = {})
7
+ check_existing_properties(name, SimplyCouch::Model::BelongsTo::Property)
8
+ parent = options[:class_name] || name.to_s.camelize
9
+ self.name.property_name.pluralize
10
+
11
+ map_definition_without_deleted = <<-eos
12
+ function(doc) {
13
+ if (doc['ruby_class'] == '#{parent}') {
14
+ if(typeof(doc['']))
15
+ if (doc['#{soft_delete_attribute}'] && doc['#{soft_delete_attribute}'] != null){
16
+ // "soft" deleted
17
+ }else{
18
+ emit([doc.#{name.to_s}_id, doc.created_at], 1);
19
+ }
20
+ }
21
+ }
22
+ eos
23
+
24
+ reduce_definition = "_sum"
25
+
26
+ view "association_#{self.name.underscore.gsub('/', '__')}_embedded_in_#{name}",
27
+ :map_function => map_definition_without_deleted,
28
+ :reduce_function => reduce_definition,
29
+ :type => :custom,
30
+ :include_docs => true
31
+
32
+ map_definition_with_deleted = <<-eos
33
+ function(doc) {
34
+ if (doc['ruby_class'] == '#{self.to_s}' && doc['#{name.to_s}_id'] != null) {
35
+ emit([doc.#{name.to_s}_id, doc.created_at], 1);
36
+ }
37
+ }
38
+ eos
39
+
40
+ view "association_#{self.name.underscore.gsub('/', '__')}_embedded_in_#{name}_with_deleted",
41
+ :map_function => map_definition_with_deleted,
42
+ :reduce_function => reduce_definition,
43
+ :type => :custom,
44
+ :include_docs => true
45
+
46
+ properties << SimplyCouch::Model::EmbeddedIn::Property.new(self, name, options)
47
+ end
48
+
49
+ class Property #:nodoc:
50
+ attr_accessor :name, :options
51
+
52
+ def initialize(owner_clazz, name, options = {})
53
+ @name = name
54
+ embedded_in_name = name
55
+ @options = {
56
+ :class_name => name.to_s.singularize.camelize
57
+ }.update(options)
58
+
59
+ @options.assert_valid_keys(:class_name)
60
+
61
+ # For now restrictions on naming
62
+ parent_property_name = owner_clazz.name.property_name.pluralize
63
+
64
+ owner_clazz.class_eval do
65
+ property :"#{name}_id"
66
+ attr_accessor :parent_object
67
+ property :index
68
+ @@embedded_in_class_name = name.to_s.camelize
69
+
70
+ class << self
71
+
72
+ define_method :embedded_in_class_name do
73
+ # embedded_in_name.to_s.singularize.camelize
74
+ @@embedded_in_class_name
75
+ end
76
+
77
+ define_method :belongs_to do |belongs_to_name, *args|
78
+ super(*([belongs_to_name] + args))
79
+ # Now override belongs to view
80
+ view "association_#{foreign_property}_belongs_to_#{belongs_to_name}",
81
+ :map_function => %|function(doc){if(doc['ruby_class'] == '#{embedded_in_class_name}' && doc['#{self.name.property_name.pluralize}']){
82
+ for(var i in doc.#{self.name.property_name.pluralize}){
83
+ if(doc['#{self.name.property_name.pluralize}'][i]['#{belongs_to_name.to_s.foreign_key}']){
84
+ emit([doc['#{self.name.property_name.pluralize}'][i]['#{belongs_to_name.to_s.foreign_key}'], doc['created_at']], doc['#{self.name.property_name.pluralize}'][i]);
85
+ }
86
+ }
87
+ }}|,
88
+ :reduce_function => %|function(key, values){return values.length}|,
89
+ :type => :raw,
90
+ :results_filter => lambda{|results| results['rows'].map{|row| d = row['value']; d.parent_object = row['doc']; d.parent_object.send(self.name.property_name.pluralize)[d.index]}},
91
+ :include_docs => true
92
+ end
93
+
94
+ define_method :count do |options = {}|
95
+ database.view(all_documents_for_count(options.merge(:reduce => true)))['rows'].try(:first).try('[]', 'value').to_i
96
+ end
97
+ end
98
+
99
+ # Make parent object send through original for callbacks
100
+ define_method :parent_object= do |value|
101
+ return @parent_object if @parent_object && @parent_object == value
102
+ @parent_object = value # Prevent circular calls
103
+ send("#{name}=", value)
104
+ end
105
+ # Redefine the equality method, since we are different kind of objects
106
+ define_method "==" do |value|
107
+ self.class == value.class && (value.respond_to?(:parent_object) && self.parent_object == value.parent_object) && (value.respond_to?(:index) && self.index == value.index)
108
+ end
109
+ view :all_documents_for_count, :type => :raw, :include_docs => false, :map_function => %|function(doc){
110
+ if(doc['ruby_class'] == '#{name.to_s.singularize.camelize}' && typeof(doc['#{parent_property_name}']) == 'object'){
111
+ for(var i=0; i < doc['#{parent_property_name}'].length; i++){
112
+ emit(doc['#{parent_property_name}'][i]['created_at'], 1);
113
+ }
114
+ }
115
+ }|, :reduce_function => '_sum'
116
+ view :all_documents, :type => :raw, :include_docs => true, :map_function => %|function(doc){
117
+ if(doc['ruby_class'] == '#{name.to_s.singularize.camelize}' && typeof(doc['#{parent_property_name}']) == 'object'){
118
+ for(var i=0; i < doc['#{parent_property_name}'].length; i++){
119
+ emit(doc['#{parent_property_name}'][i]['created_at'], doc['#{parent_property_name}'][i]);
120
+ }
121
+ }
122
+ }|, :results_filter => lambda{|results| results['rows'].map{|row| d = row['value']; d.parent_object = row['doc']; d.parent_object.send(parent_property_name)[d.index]}}
123
+
124
+
125
+
126
+ # For now empty merge. Since value of map function is transformed to object
127
+ define_method :merge do |*args|
128
+ end
129
+
130
+ define_method :save do |callbacks=true|
131
+ if !parent_object
132
+ errors.add(name, 'no_parent')
133
+ return false
134
+ end
135
+ if callbacks
136
+ _run_save_callbacks do
137
+ parent_object.is_dirty if self.dirty?
138
+ parent_object.save
139
+ end
140
+ else
141
+ parent_object.is_dirty if self.dirty?
142
+ parent_object.save
143
+ end
144
+ end
145
+
146
+ define_method name do |*args|
147
+ local_options = args.last.is_a?(Hash) ? args.last : {}
148
+ local_options.assert_valid_keys(:force_reload, :with_deleted)
149
+ forced_reload = local_options[:force_reload] || false
150
+ with_deleted = local_options[:with_deleted] || false
151
+ return parent_object
152
+ end
153
+
154
+ define_method "#{name}=" do |value|
155
+ return value if instance_variable_get("@#{name}") == value
156
+ klass = self.class.get_class_from_name(name)
157
+ raise ArgumentError, "expected #{klass} got #{value.class}" unless value.nil? || value.is_a?(klass)
158
+
159
+ if value
160
+ # Has many object update
161
+ value_has_many_name = klass.properties.find{|p| p.is_a?(SimplyCouch::Model::HasManyEmbedded::Property) && p.options[:class_name] == self.class.name}.try(:name)
162
+ value.send("add_#{value_has_many_name.to_s.singularize}", self) unless !value_has_many_name || value.send(value_has_many_name).include?(self)
163
+
164
+ # Has one object update
165
+ #value_has_one_name = klass.properties.find{|p| p.is_a?(SimplyCouch::Model::HasOneEmbedded::Property) && p.options[:class_name] == self.class.name}.try(:name)
166
+ #value.instance_variable_set("@#{value_has_one_name}", self) unless !value_has_one_name || value.send(value_has_one_name) == self
167
+ end
168
+
169
+ # Mark changed if appropriate
170
+ send("#{name}_will_change!") if value != parent_object
171
+
172
+ instance_variable_set('@parent_object', value)
173
+ if value.nil?
174
+ send("#{name}_id=", nil)
175
+ else
176
+ send("#{name}_id=", value.id)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ def build(object, json)
182
+ object.send "#{name}_id=", json["#{name}_id"]
183
+ end
184
+
185
+ def serialize(json, object)
186
+ json["#{name}_id"] = object.send("#{name}_id") if object.send("#{name}_id")
187
+ end
188
+ alias :value :serialize
189
+
190
+ def association?
191
+ true
192
+ end
193
+ end # Property
194
+ end
195
+ end
196
+ end