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,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
|