dolly 1.1.7 → 3.1.1

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 (89) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +78 -0
  3. data/lib/dolly/attachment.rb +29 -0
  4. data/lib/dolly/bulk_document.rb +27 -26
  5. data/lib/dolly/class_methods_delegation.rb +15 -0
  6. data/lib/dolly/collection.rb +32 -65
  7. data/lib/dolly/configuration.rb +35 -10
  8. data/lib/dolly/connection.rb +93 -22
  9. data/lib/dolly/depracated_database.rb +24 -0
  10. data/lib/dolly/document.rb +61 -208
  11. data/lib/dolly/document_creation.rb +27 -0
  12. data/lib/dolly/document_state.rb +66 -0
  13. data/lib/dolly/document_type.rb +47 -0
  14. data/lib/dolly/exceptions.rb +32 -0
  15. data/lib/dolly/framework_helper.rb +7 -0
  16. data/lib/dolly/identity_properties.rb +29 -0
  17. data/lib/dolly/mango.rb +156 -0
  18. data/lib/dolly/mango_index.rb +73 -0
  19. data/lib/dolly/properties.rb +36 -0
  20. data/lib/dolly/property.rb +76 -46
  21. data/lib/dolly/property_manager.rb +53 -0
  22. data/lib/dolly/property_set.rb +23 -0
  23. data/lib/dolly/query.rb +63 -75
  24. data/lib/dolly/query_arguments.rb +35 -0
  25. data/lib/dolly/request.rb +12 -107
  26. data/lib/dolly/request_header.rb +26 -0
  27. data/lib/dolly/timestamp.rb +24 -0
  28. data/lib/dolly/version.rb +1 -1
  29. data/lib/dolly/view_query.rb +21 -0
  30. data/lib/dolly.rb +2 -23
  31. data/lib/{dolly → railties}/railtie.rb +2 -1
  32. data/lib/refinements/hash_refinements.rb +27 -0
  33. data/lib/refinements/string_refinements.rb +28 -0
  34. data/lib/tasks/db.rake +27 -4
  35. data/test/bulk_document_test.rb +8 -5
  36. data/test/document_test.rb +130 -95
  37. data/test/document_type_test.rb +28 -0
  38. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  39. data/test/dummy/log/test.log +46417 -46858
  40. data/test/inheritance_test.rb +23 -0
  41. data/test/mango_index_test.rb +64 -0
  42. data/test/mango_test.rb +273 -0
  43. data/test/property_manager_test.rb +18 -0
  44. data/test/test_helper.rb +63 -18
  45. data/test/view_query_test.rb +27 -0
  46. metadata +67 -140
  47. data/Rakefile +0 -11
  48. data/lib/dolly/bulk_error.rb +0 -16
  49. data/lib/dolly/db_config.rb +0 -20
  50. data/lib/dolly/interpreter.rb +0 -5
  51. data/lib/dolly/logger.rb +0 -9
  52. data/lib/dolly/name_space.rb +0 -28
  53. data/lib/dolly/timestamps.rb +0 -21
  54. data/lib/exceptions/dolly.rb +0 -47
  55. data/test/collection_test.rb +0 -59
  56. data/test/configuration_test.rb +0 -9
  57. data/test/dummy/README.rdoc +0 -28
  58. data/test/dummy/Rakefile +0 -6
  59. data/test/dummy/app/assets/javascripts/application.js +0 -13
  60. data/test/dummy/app/assets/stylesheets/application.css +0 -13
  61. data/test/dummy/app/controllers/application_controller.rb +0 -5
  62. data/test/dummy/app/helpers/application_helper.rb +0 -2
  63. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  64. data/test/dummy/bin/bundle +0 -3
  65. data/test/dummy/bin/rails +0 -4
  66. data/test/dummy/bin/rake +0 -4
  67. data/test/dummy/config/application.rb +0 -27
  68. data/test/dummy/config/boot.rb +0 -5
  69. data/test/dummy/config/couchdb.yml +0 -13
  70. data/test/dummy/config/environment.rb +0 -5
  71. data/test/dummy/config/environments/development.rb +0 -29
  72. data/test/dummy/config/environments/production.rb +0 -80
  73. data/test/dummy/config/environments/test.rb +0 -36
  74. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  75. data/test/dummy/config/initializers/inflections.rb +0 -16
  76. data/test/dummy/config/initializers/mime_types.rb +0 -5
  77. data/test/dummy/config/initializers/secret_token.rb +0 -12
  78. data/test/dummy/config/initializers/session_store.rb +0 -3
  79. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  80. data/test/dummy/config/locales/en.yml +0 -23
  81. data/test/dummy/config/routes.rb +0 -56
  82. data/test/dummy/config.ru +0 -4
  83. data/test/dummy/lib/couch_rest_adapter/railtie.rb +0 -10
  84. data/test/dummy/public/404.html +0 -58
  85. data/test/dummy/public/422.html +0 -58
  86. data/test/dummy/public/500.html +0 -57
  87. data/test/dummy/public/favicon.ico +0 -0
  88. data/test/factories/factories.rb +0 -8
  89. data/test/request_test.rb +0 -25
@@ -1,217 +1,70 @@
1
- require "dolly/query"
2
- require "dolly/property"
3
- require 'dolly/timestamps'
1
+ require 'dolly/mango'
2
+ require 'dolly/mango_index'
3
+ require 'dolly/query'
4
+ require 'dolly/view_query'
5
+ require 'dolly/connection'
6
+ require 'dolly/request'
7
+ require 'dolly/depracated_database'
8
+ require 'dolly/document_state'
9
+ require 'dolly/properties'
10
+ require 'dolly/document_type'
11
+ require 'dolly/identity_properties'
12
+ require 'dolly/attachment'
13
+ require 'dolly/property_manager'
14
+ require 'dolly/timestamp'
15
+ require 'dolly/query_arguments'
16
+ require 'dolly/document_creation'
17
+ require 'dolly/class_methods_delegation'
18
+ require 'dolly/framework_helper'
19
+ require 'refinements/string_refinements'
4
20
 
5
21
  module Dolly
6
22
  class Document
7
- extend Dolly::Connection
8
- include Dolly::Query
9
- extend Dolly::Timestamps
10
-
11
- attr_accessor :rows, :doc, :key
12
- class_attribute :properties
13
- cattr_accessor :timestamps do
14
- {}
15
- end
16
-
17
- def initialize options = {}
18
- @doc ||= {}
19
- options = options.with_indifferent_access
20
- init_properties options
21
- end
22
-
23
- def persisted?
24
- !doc['_rev'].blank?
25
- end
26
-
27
- def update_properties options = {}
28
- raise InvalidProperty unless valid_properties?(options)
29
- options.each do |property, value|
30
- send(:"#{property}=", value)
31
- end
32
- end
33
-
34
- def update_properties! options = {}
35
- update_properties(options)
36
- save
37
- end
38
-
39
- def reload
40
- self.doc = self.class.find(id).doc
41
- end
42
-
43
- def id
44
- doc['_id'] ||= self.class.next_id
45
- end
46
-
47
- def id= base_value
48
- doc ||= {}
49
- doc['_id'] = self.class.namespace(base_value)
50
- end
51
-
52
- def rev
53
- doc['_rev']
54
- end
55
-
56
- def rev= value
57
- doc['_rev'] = value
58
- end
59
-
60
- def save options = {}
61
- return false unless options[:validate] == false || valid?
62
- self.doc['_id'] = self.id if self.id.present?
63
- self.doc['_id'] = self.class.next_id if self.doc['_id'].blank?
64
- set_created_at if timestamps[self.class.name]
65
- set_updated_at if timestamps[self.class.name]
66
- response = database.put(id_as_resource, self.doc.to_json)
67
- obj = JSON::parse response.parsed_response
68
- doc['_rev'] = obj['rev'] if obj['rev']
69
- obj['ok']
70
- end
71
-
72
- def save!
73
- raise DocumentInvalidError unless valid?
74
- save
75
- end
76
-
77
- def destroy hard = true
78
- if hard
79
- q = id_as_resource + "?rev=#{rev}"
80
- response = database.delete(q)
81
- JSON::parse response.parsed_response
82
- else
83
- self.doc['_deleted'] = true
84
- self.save
85
- end
86
- end
87
-
88
- def rows= col
89
- raise Dolly::ResourceNotFound if col.empty?
90
- col.each{ |r| @doc = r['doc'] }
91
- _properties.each do |p|
92
- #TODO: Refactor properties so it is not required
93
- #to be a class property. But something that doesn't
94
- #persist all the inheretence chain
95
- next unless self.respond_to? :"#{p.name}="
96
- self.send "#{p.name}=", doc[p.name]
97
- end
98
- @rows = col
99
- end
100
-
101
- def from_json string
102
- parsed = JSON::parse( string )
103
- self.rows = parsed['rows']
104
- self
105
- end
106
-
107
- def database
108
- self.class.database
109
- end
110
-
111
- def id_as_resource
112
- CGI::escape id
113
- end
114
-
115
- def attach_file! file_name, mime_type, body, opts={}
116
- attach_file file_name, mime_type, body, opts
117
- save
118
- end
119
-
120
- def attach_file file_name, mime_type, body, opts={}
121
- if opts[:inline]
122
- attach_inline_file file_name, mime_type, body
123
- else
124
- attach_standalone_file file_name, mime_type, body
125
- end
126
- end
127
-
128
- def attach_inline_file file_name, mime_type, body
129
- attachment_data = { file_name.to_s => { 'content_type' => mime_type,
130
- 'data' => Base64.encode64(body)} }
131
- doc['_attachments'] ||= {}
132
- doc['_attachments'].merge! attachment_data
133
- end
134
-
135
- def attach_standalone_file file_name, mime_type, body
136
- database.attach id_as_resource, CGI.escape(file_name), body, { 'Content-Type' => mime_type }
137
- end
138
-
139
- def self.create options = {}
140
- obj = new options
141
- obj.save
142
- obj
143
- end
144
-
145
- def self.property *ary
146
- self.properties ||= {}
147
- options = ary.pop if ary.last.kind_of? Hash
148
- options ||= {}
149
-
150
- ary.each do |name|
151
- self.properties[name] = Property.new options.merge(name: name)
152
- self.write_methods name
23
+ extend Mango
24
+ extend Query
25
+ extend ViewQuery
26
+ extend Request
27
+ extend DepracatedDatabase
28
+ extend Properties
29
+ extend DocumentCreation
30
+
31
+ include DocumentType
32
+ include PropertyManager
33
+ include Timestamp
34
+ include DocumentState
35
+ include IdentityProperties
36
+ include Attachment
37
+ include QueryArguments
38
+ include ClassMethodsDelegation
39
+ include FrameworkHelper
40
+
41
+ attr_writer :doc
42
+
43
+ def initialize(attributes = {})
44
+ init_ancestor_properties
45
+ properties.each(&build_property(attributes))
46
+ end
47
+
48
+ protected
49
+
50
+ def doc
51
+ @doc ||= doc_for_framework
52
+ end
53
+
54
+ def init_ancestor_properties
55
+ self.class.ancestors.map do |ancestor|
56
+ begin
57
+ ancestor.properties.entries.each do |property|
58
+ properties << property
59
+ end
60
+ rescue NoMethodError => e
61
+ end
153
62
  end
154
63
  end
155
64
 
156
- private
157
- #TODO: create a PropertiesSet service object, to do all this
158
- def self.write_methods name
159
- property = properties[name]
160
- define_method(name) { read_property name }
161
- define_method("#{name}=") { |value| write_property name, value }
162
- define_method(:"#{name}?") { send name } if property.boolean?
163
- define_method("[]") {|n| send n.to_sym }
164
- end
165
-
166
- def write_property name, value
167
- instance_variable_set(:"@#{name}", value)
168
- @doc[name.to_s] = value
65
+ def doc_for_framework
66
+ return {} unless rails?
67
+ {}.with_indifferent_access
169
68
  end
170
-
171
- def read_property name
172
- if instance_variable_get(:"@#{name}").nil?
173
- write_property name, (doc[name.to_s] || self.properties[name].value)
174
- end
175
- instance_variable_get(:"@#{name}")
176
- end
177
-
178
- def _properties
179
- self.properties.values
180
- end
181
-
182
- def init_properties options = {}
183
- raise Dolly::ResourceNotFound if options['error'] == 'not_found'
184
- options.each do |k, v|
185
- next unless respond_to? :"#{k}="
186
- send(:"#{k}=", v)
187
- end
188
- initialize_default_properties options if self.properties.present?
189
- init_doc options
190
- end
191
-
192
- def initialize_default_properties options
193
- _properties.reject { |property| options.keys.include? property.name }.each do |property|
194
- property_value = property.default.clone unless Dolly::Property::CANT_CLONE.any? { |klass| property.default.is_a? klass }
195
- property_value ||= property.default
196
- self.doc[property.name] ||= property_value
197
- end
198
- end
199
-
200
- def init_doc options
201
- self.doc ||= {}
202
- #TODO: define what will be the preference _id or id
203
- normalized_id = options[:_id] || options[:id]
204
- self.doc['_id'] = self.class.namespace( normalized_id ) if normalized_id
205
- end
206
-
207
- def valid_properties?(options)
208
- options.keys.any?{ |option| properties_include?(option.to_s) }
209
- end
210
-
211
- def properties_include? property
212
- _properties.map(&:name).include? property
213
- end
214
-
215
- def valid?; true; end
216
69
  end
217
70
  end
@@ -0,0 +1,27 @@
1
+ require 'dolly/properties'
2
+ require 'dolly/framework_helper'
3
+
4
+ module Dolly
5
+ module DocumentCreation
6
+ include Properties
7
+ include FrameworkHelper
8
+
9
+ def from_doc(doc)
10
+ attributes = property_clean_doc(doc)
11
+
12
+ new(attributes).tap do |model|
13
+ model.send(:doc).merge!(doc)
14
+ end
15
+ end
16
+
17
+ def from_json(json)
18
+ raw_data = Oj.load(json, symbol_keys: true)
19
+ data = rails? ? data.with_indifferent_access : raw_data
20
+ from_doc(data)
21
+ end
22
+
23
+ def create(attributes)
24
+ new(attributes).tap { |model| model.save }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,66 @@
1
+ require 'dolly/class_methods_delegation'
2
+
3
+ module Dolly
4
+ module DocumentState
5
+ include ClassMethodsDelegation
6
+
7
+ def save(options = {})
8
+ return false unless options[:validate] == false || valid?
9
+ set_type if typed? && type.nil?
10
+ write_timestamps(persisted?)
11
+ after_save(connection.put(id, doc))
12
+ end
13
+
14
+ def save!
15
+ raise DocumentInvalidError unless valid?
16
+ save
17
+ end
18
+
19
+ def update_properties(properties)
20
+ properties.each(&update_attribute)
21
+ end
22
+
23
+ def update_properties!(properties)
24
+ update_properties(properties)
25
+ save!
26
+ end
27
+
28
+ def destroy is_hard = true
29
+ return connection.delete(id, rev) if is_hard
30
+ doc[:_deleted] = true
31
+ save
32
+ rescue Dolly::ResourceNotFound
33
+ nil
34
+ rescue Dolly::ServerError => error
35
+ raise error unless error.message =~ /conflict/
36
+ self.rev = self.class.safe_find(id)&.rev
37
+ return unless self.rev
38
+ destroy(is_hard)
39
+ end
40
+
41
+ def reload
42
+ reloaded_doc = self.class.find(id).send(:doc)
43
+ attributes = property_clean_doc(reloaded_doc)
44
+
45
+ attributes.each(&update_attribute)
46
+ end
47
+
48
+ def persisted?
49
+ return false unless doc[:_rev]
50
+ !doc[:_rev].empty?
51
+ end
52
+
53
+ def to_h
54
+ doc
55
+ end
56
+
57
+ def valid?
58
+ true
59
+ end
60
+
61
+ def after_save(response)
62
+ self.rev = response[:rev]
63
+ response[:ok]
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,47 @@
1
+ require 'refinements/string_refinements'
2
+
3
+ module Dolly
4
+ module DocumentType
5
+ using StringRefinements
6
+
7
+ def namespace_keys(keys)
8
+ keys.map { |key| namespace_key key }
9
+ end
10
+
11
+ def namespace_key(key)
12
+ return key if key =~ %r{^#{name_paramitized}/}
13
+ "#{name_paramitized}/#{key}"
14
+ end
15
+
16
+ def base_id
17
+ self.id.sub(%r{^#{name_paramitized}/}, '')
18
+ end
19
+
20
+ def name_paramitized
21
+ class_name.split("::").last.underscore
22
+ end
23
+
24
+ def class_name
25
+ is_a?(Class) ? name : self.class.name
26
+ end
27
+
28
+ def typed?
29
+ respond_to?(:type)
30
+ end
31
+
32
+ def set_type
33
+ return unless typed?
34
+ write_attribute(:type, name_paramitized)
35
+ end
36
+
37
+ def self.included(base)
38
+ base.extend(ClassMethods)
39
+ end
40
+
41
+ module ClassMethods
42
+ def typed_model
43
+ property :type, class_name: String
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,32 @@
1
+ module Dolly
2
+ class ResourceNotFound < RuntimeError
3
+ def to_s
4
+ 'The document was not found.'
5
+ end
6
+ end
7
+
8
+ class ServerError < RuntimeError
9
+ def initialize msg
10
+ @msg = msg
11
+ end
12
+
13
+ def to_s
14
+ "There has been an error on the couchdb server: #{@msg.inspect}"
15
+ end
16
+ end
17
+
18
+ class InvalidMangoOperatorError < RuntimeError
19
+ def initialize msg
20
+ @msg = msg
21
+ end
22
+
23
+ def to_s
24
+ "Invalid Mango operator: #{@msg.inspect}"
25
+ end
26
+ end
27
+
28
+ class IndexNotFoundError < RuntimeError; end
29
+ class InvalidConfigFileError < RuntimeError; end
30
+ class InvalidProperty < RuntimeError; end
31
+ class DocumentInvalidError < RuntimeError; end
32
+ end
@@ -0,0 +1,7 @@
1
+ module Dolly
2
+ module FrameworkHelper
3
+ def rails?
4
+ defined?(ActiveSupport::HashWithIndifferentAccess)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ require 'dolly/document_type'
2
+ require 'dolly/class_methods_delegation'
3
+
4
+ module Dolly
5
+ module IdentityProperties
6
+ include DocumentType
7
+ include ClassMethodsDelegation
8
+
9
+ def id
10
+ doc[:_id] ||= namespace_key(connection.uuids.last)
11
+ end
12
+
13
+ def id= value
14
+ doc[:_id] = namespace_key(value)
15
+ end
16
+
17
+ def rev
18
+ doc[:_rev]
19
+ end
20
+
21
+ def rev= value
22
+ doc[:_rev] = value
23
+ end
24
+
25
+ def id_as_resource
26
+ CGI.escape(id)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'refinements/hash_refinements'
4
+
5
+ module Dolly
6
+ module Mango
7
+ using HashRefinements
8
+
9
+ SELECTOR_SYMBOL = '$'
10
+
11
+ COMBINATION_OPERATORS = %I[
12
+ and
13
+ or
14
+ not
15
+ nor
16
+ all
17
+ elemMatch
18
+ allMath
19
+ ].freeze
20
+
21
+ CONDITION_OPERATORS = %I[
22
+ lt
23
+ lte
24
+ eq
25
+ ne
26
+ gte
27
+ gt
28
+ exists
29
+ in
30
+ nin
31
+ size
32
+ mod
33
+ regex
34
+ ].freeze
35
+
36
+ TYPE_OPERATOR = %I[
37
+ type
38
+ $type
39
+ ]
40
+
41
+ ALL_OPERATORS = COMBINATION_OPERATORS + CONDITION_OPERATORS
42
+
43
+ DESIGN = '_find'
44
+
45
+ def find_by(query, opts = {})
46
+ build_model_from_doc(find_doc_by(query, opts))
47
+ end
48
+
49
+ def find_doc_by(query, opts = {})
50
+ opts.merge!(limit: 1)
51
+ response = perform_query(build_query(query, opts))
52
+ print_index_warning(query) if response.fetch(:warning, nil)
53
+ response[:docs].first
54
+ end
55
+
56
+ def where(query, opts = {})
57
+ docs_where(query, opts).map do |doc|
58
+ build_model_from_doc(doc)
59
+ end
60
+ end
61
+
62
+ def docs_where(query, opts = {})
63
+ response = perform_query(build_query(query, opts))
64
+ print_index_warning(query) if response.fetch(:warning, nil)
65
+ response[:docs]
66
+ end
67
+
68
+ def find_bare(id, fields, options = {})
69
+ q = { _id: id }
70
+ opts = { fields: fields }.merge(options)
71
+ query = build_query(q, opts)
72
+ response = perform_query(query)
73
+ response[:docs]
74
+ end
75
+
76
+ def where_bare(selector, fields, options = {})
77
+ opts = { fields: fields }.merge(options)
78
+ query = build_query(selector, opts)
79
+ response = perform_query(query)
80
+ response[:docs]
81
+ end
82
+
83
+ def find_with_metadata(query, options = {})
84
+ opts = options.merge!(limit: 1)
85
+ perform_query(build_query(query, opts))
86
+ end
87
+
88
+ def where_with_metadata(query, options = {})
89
+ perform_query(build_query(query, options))
90
+ end
91
+
92
+ def perform_query(structured_query)
93
+ connection.post(DESIGN, structured_query)
94
+ end
95
+
96
+ private
97
+
98
+ def print_index_warning(query)
99
+ message = "Index not found for #{query.inspect}"
100
+ if (defined?(Rails.logger) && Rails&.env&.development?)
101
+ Rails.logger.info(message)
102
+ else
103
+ puts message
104
+ end
105
+ end
106
+
107
+ def build_model_from_doc(doc)
108
+ return nil if doc.nil?
109
+ new(doc.slice(*all_property_keys)).tap { |d| d.rev = doc[:_rev] }
110
+ end
111
+
112
+ def build_query(query, opts)
113
+ { 'selector' => build_selectors(query) }.merge(opts)
114
+ end
115
+
116
+ def build_selectors(query)
117
+ query.deep_transform_keys do |key|
118
+ next build_key(key) if is_operator?(key)
119
+ next key if is_type_operator?(key)
120
+ raise Dolly::InvalidMangoOperatorError.new(key) unless has_property?(key)
121
+ key
122
+ end
123
+ end
124
+
125
+ def build_key(key)
126
+ return key if key.to_s.starts_with?(SELECTOR_SYMBOL)
127
+ "#{SELECTOR_SYMBOL}#{key}"
128
+ end
129
+
130
+ def is_operator?(key)
131
+ ALL_OPERATORS.include?(key) || key.to_s.starts_with?(SELECTOR_SYMBOL)
132
+ end
133
+
134
+ def fetch_fields(query)
135
+ deep_keys(query).reject do |key|
136
+ is_operator?(key) || is_type_operator?(key)
137
+ end
138
+ end
139
+
140
+ def has_property?(key)
141
+ self.all_property_keys.include?(key)
142
+ end
143
+
144
+ def is_type_operator?(key)
145
+ TYPE_OPERATOR.include?(key.to_sym)
146
+ end
147
+
148
+ def deep_keys(obj)
149
+ case obj
150
+ when Hash then obj.keys + obj.values.flat_map { |v| deep_keys(v) }
151
+ when Array then obj.flat_map { |i| deep_keys(i) }
152
+ else []
153
+ end
154
+ end
155
+ end
156
+ end