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,106 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module View
4
+ # A view to return model instances by searching its properties.
5
+ # If you pass reduce => true will count instead
6
+ #
7
+ # example:
8
+ # view :my_view, :key => :name
9
+ #
10
+ # in addition you can pass in conditions as a javascript string
11
+ # view :my_view_only_completed, :key => :name, :conditions => 'doc.completed = true'
12
+ # and also a results filter (the results will be run through the given proc):
13
+ # view :my_view, :key => :name, :results_filter => lambda{|results| results.size}
14
+ class ModelViewSpec < BaseViewSpec
15
+ # The key simply_couch uses for class identification in CouchDB documents
16
+ RUBY_CLASS_KEY = 'ruby_class'
17
+
18
+ class JavascriptGenerator
19
+ def initialize(options, klass)
20
+ @options = options
21
+ @klass = klass
22
+ end
23
+
24
+ def map_body(&block)
25
+ <<-JS
26
+ function(doc) {
27
+ if(doc.#{RUBY_CLASS_KEY} && doc.#{RUBY_CLASS_KEY} == '#{@klass.name}'#{conditions}) {
28
+ #{yield}
29
+ }
30
+ }
31
+ JS
32
+ end
33
+
34
+ def map_function
35
+ map_body do
36
+ "emit(#{formatted_key}, #{emit_value});"
37
+ end
38
+ end
39
+
40
+ def formatted_key(_key = nil)
41
+ _key ||= @options[:key]
42
+ if _key.is_a? Array
43
+ '[' + _key.map{|key_part| formatted_key(key_part)}.join(', ') + ']'
44
+ else
45
+ "doc['#{_key}']"
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # Allow custom emit values. Raise when the specified argument is not recognized
52
+ def emit_value
53
+ case @options[:emit_value]
54
+ when Symbol then "doc['#{@options[:emit_value]}']"
55
+ when String then @options[:emit_value]
56
+ when Numeric then @options[:emit_value]
57
+ when NilClass then 1
58
+ else
59
+ raise "The emit value specified is not recognized"
60
+ end
61
+ end
62
+
63
+ def conditions
64
+ " && (#{@options[:conditions]})" if @options[:conditions]
65
+ end
66
+ end
67
+
68
+ delegate :map_function, :map_body, :formatted_key, :to => :generator
69
+
70
+ def view_parameters
71
+ _super = super
72
+ if _super[:reduce]
73
+ _super
74
+ else
75
+ {:include_docs => true, :reduce => false}.merge(_super)
76
+ end
77
+ end
78
+
79
+ def reduce_function
80
+ "_sum"
81
+ end
82
+
83
+ def process_results(results)
84
+ processed = if count?
85
+ results['rows'].first.try(:[], 'value') || 0
86
+ else
87
+ results['rows'].map {|row|
88
+ row['doc'] || (row['id'] unless view_parameters[:include_docs])
89
+ }.compact
90
+ end
91
+ super processed
92
+ end
93
+
94
+ private
95
+
96
+ def generator
97
+ @generator ||= JavascriptGenerator.new(@options, @klass)
98
+ end
99
+
100
+ def count?
101
+ view_parameters[:reduce]
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,53 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module View
4
+ # A view to return model instances with only some properties populated by searching its properties, e.g. for very large documents where you are only interested in some of their data
5
+ #
6
+ # example:
7
+ # view :my_view, :key => :name, :properties => [:name, :author], :type => :properties
8
+ class PropertiesViewSpec < ModelViewSpec
9
+ def map_function
10
+ map_body do
11
+ "emit(#{formatted_key}, #{properties_for_map(properties)});"
12
+ end
13
+ end
14
+
15
+ def reduce_function
16
+ <<-JS
17
+ function(key, values, rereduce) {
18
+ if(rereduce) {
19
+ return sum(values);
20
+ } else {
21
+ return values.length;
22
+ }
23
+ }
24
+ JS
25
+ end
26
+
27
+ def process_results(results)
28
+ results['rows'].map do |row|
29
+ klass.json_create row['value'].merge(:_id => row['id'])
30
+ end
31
+ end
32
+
33
+ def view_parameters
34
+ {:include_docs => false}.merge(super)
35
+ end
36
+
37
+ def language
38
+ :javascript
39
+ end
40
+
41
+ private
42
+
43
+ def properties
44
+ options[:properties]
45
+ end
46
+
47
+ def properties_for_map(properties)
48
+ '{' + properties.map{|p| "#{p}: doc.#{p}"}.join(', ') + '}'
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module View
4
+ # A view for custom map/reduce functions that returns the raw data from couchdb
5
+ #
6
+ # example:
7
+ # view :my_custom_view, :map => "function(doc) { emit(doc._id, null); }", :type => :raw, :reduce => nil
8
+ # optionally you can pass in a results filter which you can use to process the raw couchdb results before returning them
9
+ #
10
+ # example:
11
+ # view :my_custom_view, :map => "function(doc) { emit(doc._id, null); }", :type => :raw, :results_filter => lambda{|results| results['rows'].map{|row| row['value']}}
12
+ #
13
+ # example:
14
+ # view :my_custom_view, :map => "function(doc) { emit(doc._id, null); }", :type => :raw, :lib => {:module => "exports.name = 'module';"}
15
+ class RawViewSpec < BaseViewSpec
16
+ def map_function
17
+ options[:map]
18
+ end
19
+
20
+ def reduce_function
21
+ options[:reduce]
22
+ end
23
+
24
+ def lib
25
+ options[:lib]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,98 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module View
4
+ # Used to query views (and create them if they don't exist). Usually you won't have to use this class directly.
5
+ class ViewQuery
6
+ def initialize(couchrest_database, design_document_name, view, list = nil, lib = nil, language = :javascript)
7
+ @database = couchrest_database
8
+ @design_document_name = design_document_name
9
+ @view_name = view.keys[0]
10
+ @map_function = view.values[0][:map]
11
+ @reduce_function = view.values[0][:reduce]
12
+ @lib = lib
13
+ @language = language
14
+ if list
15
+ @list_function = list.values[0]
16
+ @list_name = list.keys[0]
17
+ end
18
+ end
19
+
20
+ def query_view!(parameters = {})
21
+ update_view unless view_has_been_updated?
22
+ begin
23
+ query_view parameters
24
+ rescue CouchRest::NotFound => e
25
+ update_view
26
+ retry
27
+ end
28
+ end
29
+
30
+ # mainly useful for testing where you drop the database between tests.
31
+ # only after clearing the cache design docs will be updated/re-created.
32
+ def self.clear_cache
33
+ __updated_views.clear
34
+ end
35
+
36
+ def self.__updated_views
37
+ @updated_views ||= {}
38
+ @updated_views
39
+ end
40
+
41
+ private
42
+
43
+ def update_view
44
+ design_doc = @database.get "_design/#{@design_document_name}" rescue nil
45
+ original_views = design_doc && design_doc['views'].dup
46
+ original_lists = design_doc && design_doc['lists'] && design_doc['lists'].dup
47
+ view_updated unless design_doc.nil?
48
+ design_doc ||= empty_design_document
49
+ design_doc['views'][@view_name.to_s] = view_functions
50
+ if @lib
51
+ design_doc['views']['lib'] = (design_doc['views']['lib'] || {}).merge(@lib)
52
+ end
53
+ if @list_function
54
+ design_doc['lists'] ||= {}
55
+ design_doc['lists'][@list_name.to_s] = @list_function
56
+ end
57
+ @database.save_doc(design_doc) if original_views != design_doc['views'] || original_lists != design_doc['lists']
58
+ end
59
+
60
+ def view_functions
61
+ if @reduce_function
62
+ {'map' => @map_function, 'reduce' => @reduce_function}
63
+ else
64
+ {'map' => @map_function}
65
+ end
66
+ end
67
+
68
+ def empty_design_document
69
+ {'views' => {}, 'lists' => {}, "_id" => "_design/#{@design_document_name}", "language" => @language.to_s}
70
+ end
71
+
72
+ def view_has_been_updated?
73
+ updated_views[[@design_document_name, @view_name]]
74
+ end
75
+
76
+ def view_updated
77
+ updated_views[[@design_document_name, @view_name]] = true
78
+ end
79
+
80
+ def updated_views
81
+ self.class.__updated_views
82
+ end
83
+
84
+ def query_view(parameters)
85
+ if @list_name
86
+ @database.connection.get CouchRest.paramify_url("/#{@database.name}/_design/#{@design_document_name}/_list/#{@list_name}/#{@view_name}", parameters)
87
+ else
88
+ @database.view view_url, parameters
89
+ end
90
+ end
91
+
92
+ def view_url
93
+ "#{@design_document_name}/#{@view_name}"
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,8 @@
1
+ require 'simply_couch/model/view/base_view_spec'
2
+ require 'simply_couch/model/view/model_view_spec'
3
+ require 'simply_couch/model/view/properties_view_spec'
4
+ require 'simply_couch/model/view/custom_view_spec'
5
+ require 'simply_couch/model/view/raw_view_spec'
6
+ require 'simply_couch/model/view/view_query'
7
+ require 'simply_couch/model/view/custom_views'
8
+ require 'simply_couch/model/view/lists'
@@ -0,0 +1,26 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module Views
4
+ class ArrayPropertyViewSpec < SimplyCouch::Model::View::ModelViewSpec
5
+ def map_function
6
+ "function(doc) {
7
+ if(doc.ruby_class && doc.ruby_class == '#{@klass.name}') {
8
+ if (#{formatted_key(key)}.constructor.toString().match(/function Array()/)) {
9
+ for (var i in #{formatted_key(key)}) {
10
+ emit(#{formatted_key(key)}[i], 1);
11
+ }
12
+ } else {
13
+ emit(#{formatted_key(key)}, 1);
14
+ }
15
+ }
16
+ }"
17
+ end
18
+
19
+ def key
20
+ @options[:key]
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,43 @@
1
+ module SimplyCouch
2
+ module Model
3
+ module Views
4
+ class DeletedModelViewSpec < SimplyCouch::Model::View::ModelViewSpec
5
+ def map_function
6
+ <<-eos
7
+ function(doc) {
8
+ if (doc.ruby_class && doc.ruby_class == '#{@klass.name}') {
9
+ if (doc['#{@klass.soft_delete_attribute}'] && doc['#{@klass.soft_delete_attribute}'] != null){
10
+ // "soft" deleted
11
+ }else{
12
+ emit(doc.created_at, 1);
13
+ }
14
+ }
15
+ }
16
+ eos
17
+ end
18
+
19
+ def reduce_function
20
+ '_sum'
21
+ end
22
+
23
+ def view_parameters
24
+ _super = super
25
+ if _super[:reduce]
26
+ _super
27
+ else
28
+ {:include_docs => true, :reduce => false}.merge(_super)
29
+ end
30
+ end
31
+
32
+ def process_results(results)
33
+ if count?
34
+ results['rows'].first.try(:[], 'value') || 0
35
+ else
36
+ results['rows'].map { |row| row['doc'] || row['id'] }
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,2 @@
1
+ require 'simply_couch/model/views/array_property_view_spec'
2
+ require 'simply_couch/model/views/deleted_model_view_spec'
@@ -0,0 +1,195 @@
1
+ require 'active_model'
2
+
3
+ CouchRest.decode_json_objects = true
4
+ require 'json'
5
+ JSON.create_id = 'ruby_class'
6
+
7
+ require 'active_support'
8
+ unless {}.respond_to?(:assert_valid_keys)
9
+ require 'active_support/core_ext'
10
+ end
11
+ I18n.load_path << File.join(File.expand_path(File.dirname(__FILE__)), 'locale', 'en.yml')
12
+ require File.expand_path(File.dirname(__FILE__) + '/../simply_couch')
13
+ require 'simply_couch/model/database'
14
+ require 'simply_couch/model/validations'
15
+ require 'simply_couch/model/pagination_options'
16
+ require 'simply_couch/model/association_property'
17
+ require 'simply_couch/model/properties'
18
+ require 'simply_couch/model/ancestry'
19
+ require 'simply_couch/model/finders'
20
+ require 'simply_couch/model/find_by'
21
+ require 'simply_couch/model/belongs_to'
22
+ require 'simply_couch/model/embedded_in'
23
+ require 'simply_couch/model/has_many'
24
+ require 'simply_couch/model/has_many_embedded'
25
+ require 'simply_couch/model/has_and_belongs_to_many'
26
+ require 'simply_couch/model/has_one'
27
+ require 'simply_couch/model/attachments'
28
+ require 'simply_couch/model/pagination'
29
+ require 'simply_couch/model/persistence'
30
+ require 'simply_couch/model/view'
31
+ require 'simply_couch/model/views'
32
+ require 'simply_couch/include_relation'
33
+
34
+ module SimplyCouch
35
+ module Model
36
+ def self.included(clazz)
37
+ clazz.send(:include, Persistence)
38
+ clazz.send(:include, InstanceMethods)
39
+ clazz.send(:extend, ClassMethods)
40
+
41
+ clazz.instance_eval do
42
+ attr_accessor :_accessible_attributes, :_protected_attributes
43
+
44
+ view :all_documents, :key => :created_at
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+ include SimplyCouch::ClassMethods::Base
50
+ include SimplyCouch::Model::Database
51
+ include SimplyCouch::Model::Validations
52
+ include SimplyCouch::Model::BelongsTo
53
+ include SimplyCouch::Model::EmbeddedIn
54
+ include SimplyCouch::Model::HasMany
55
+ include SimplyCouch::Model::HasManyEmbedded
56
+ include SimplyCouch::Model::HasAndBelongsToMany
57
+ include SimplyCouch::Model::HasOne
58
+ include SimplyCouch::Model::Finders
59
+ include SimplyCouch::Model::FindBy
60
+ include SimplyCouch::Model::Pagination
61
+ include SimplyCouch::Model::PaginationOptions
62
+ include SimplyCouch::Storage::ClassMethods
63
+ include SimplyCouch::Model::Ancestry
64
+
65
+ def create(attributes = {}, &blk)
66
+ instance = new(attributes, &blk)
67
+ instance.save
68
+ instance
69
+ end
70
+
71
+ def create!(attributes = {}, &blk)
72
+ instance = new(attributes, &blk)
73
+ instance.save!
74
+ instance
75
+ end
76
+
77
+ def enable_soft_delete(property_name = :deleted_at)
78
+ @_soft_delete_attribute = property_name.to_sym
79
+ property property_name, :type => Time
80
+ _define_hard_delete_methods
81
+ _define_soft_delete_views
82
+ end
83
+
84
+ def soft_delete_attribute
85
+ @_soft_delete_attribute
86
+ end
87
+
88
+ def soft_deleting_enabled?
89
+ !soft_delete_attribute.nil?
90
+ end
91
+
92
+ def split_design_documents_per_view(enabled = true)
93
+ @_split_design_documents = enabled
94
+ end
95
+
96
+ def split_design_documents?
97
+ @_split_design_documents || false
98
+ end
99
+
100
+ def auto_conflict_resolution_on_save
101
+ @auto_conflict_resolution_on_save.nil? ? true : @auto_conflict_resolution_on_save
102
+ end
103
+
104
+ def auto_conflict_resolution_on_save=(val)
105
+ @auto_conflict_resolution_on_save = val
106
+ end
107
+
108
+ def method_missing(name, *args)
109
+ if name.to_s =~ /^find_by/
110
+ _define_find_by(name, *args)
111
+ elsif name.to_s =~ /^find_all_by/
112
+ _define_find_all_by(name, *args)
113
+ elsif name.to_s =~ /^count_by/
114
+ _define_count_by(name, *args)
115
+ else
116
+ super
117
+ end
118
+ end
119
+
120
+ def _define_hard_delete_methods
121
+ define_method("destroy!") do
122
+ destroy(true)
123
+ end
124
+
125
+ define_method("delete!") do
126
+ destroy(true)
127
+ end
128
+ end
129
+
130
+ def _define_soft_delete_views
131
+ view :all_documents_without_deleted, :type => SimplyCouch::Model::Views::DeletedModelViewSpec
132
+ end
133
+
134
+ def _define_cache_accessors(name, options)
135
+ define_method "_get_cached_#{name}" do
136
+ instance_variable_get("@#{name}") || {}
137
+ end
138
+
139
+ define_method "_set_cached_#{name}" do |value, cache_key|
140
+ cached = send("_get_cached_#{name}")
141
+ cached[cache_key] = value
142
+ instance_variable_set("@#{name}", cached)
143
+ end
144
+
145
+ define_method "_cache_key_for" do |opt|
146
+ opt.blank? ? :all : opt.to_s
147
+ end
148
+ end
149
+ end
150
+
151
+ def extract_association_options(local_options = nil)
152
+ forced_reload = false
153
+ with_deleted = false
154
+ limit = nil
155
+ descending = false
156
+ skip = nil
157
+
158
+ if local_options
159
+ local_options.assert_valid_keys(:force_reload, :with_deleted, :limit, :order)
160
+ forced_reload = local_options.delete(:force_reload)
161
+ with_deleted = local_options[:with_deleted]
162
+ limit = local_options[:limit]
163
+ descending = (local_options[:order] == :desc) ? true : false
164
+ skip = local_options[:skip]
165
+ end
166
+ return [forced_reload, with_deleted, limit, descending, skip]
167
+ end
168
+
169
+ def self.delete_all_design_documents(database)
170
+ db = CouchRest.database(database)
171
+ db.info # ensure DB exists
172
+ design_docs = CouchRest.get("#{database}/_all_docs?startkey=%22_design%22&endkey=%22_design0%22")['rows'].map do |row|
173
+ [row['id'], row['value']['rev']]
174
+ end
175
+ design_docs.each do |doc_id, rev|
176
+ db.delete_doc({'_id' => doc_id, '_rev' => rev})
177
+ end
178
+ design_docs.size
179
+ end
180
+
181
+ def self.compact_all_design_documents(database)
182
+ db = CouchRest.database(database)
183
+ db.info # ensure DB exists
184
+ design_docs = CouchRest.get("#{database}/_all_docs?startkey=%22_design%22&endkey=%22_design0%22")['rows'].map do |row|
185
+ [row['id'], row['value']['rev']]
186
+ end
187
+ design_docs.each do |doc_id, rev|
188
+ puts "#{database}/_compact/#{doc_id.gsub("_design/",'')}"
189
+ CouchRest.post("#{database}/_compact/#{doc_id.gsub("_design/",'')}")
190
+ end
191
+ design_docs.size
192
+ end
193
+
194
+ end
195
+ end
@@ -0,0 +1,23 @@
1
+ namespace :simply_couch do
2
+ desc "delete all design documents"
3
+ task :delete_design_documents do
4
+ require File.dirname(__FILE__) + "/couch"
5
+ if database = ENV['DATABASE']
6
+ deleted = SimplyCouch::Model.delete_all_design_documents(database)
7
+ puts "deleted #{deleted} design documents in #{database}"
8
+ else
9
+ puts "please specify which database to clear: DATABASE=http://localhost:5984/simply_couch rake simply_couch:delete_design_documents"
10
+ end
11
+ end
12
+
13
+ desc "compact all design documents"
14
+ task :compact_design_documents do
15
+ require File.dirname(__FILE__) + "/couch"
16
+ if database = ENV['DATABASE']
17
+ compacted = SimplyCouch::Model.compact_all_design_documents(database)
18
+ puts "triggered compaction of #{compacted} design documents in #{database}"
19
+ else
20
+ puts "please specify which database to clear: DATABASE=http://localhost:5984/simply_couch rake simply_couch:delete_design_documents"
21
+ end
22
+ end
23
+ end