dolly 1.1.5 → 3.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 +5 -5
- data/README.md +78 -0
- data/lib/dolly.rb +2 -23
- data/lib/dolly/attachment.rb +29 -0
- data/lib/dolly/bulk_document.rb +27 -26
- data/lib/dolly/class_methods_delegation.rb +15 -0
- data/lib/dolly/collection.rb +32 -65
- data/lib/dolly/configuration.rb +35 -10
- data/lib/dolly/connection.rb +86 -22
- data/lib/dolly/depracated_database.rb +24 -0
- data/lib/dolly/document.rb +48 -198
- data/lib/dolly/document_creation.rb +20 -0
- data/lib/dolly/document_state.rb +66 -0
- data/lib/dolly/document_type.rb +47 -0
- data/lib/dolly/exceptions.rb +32 -0
- data/lib/dolly/identity_properties.rb +29 -0
- data/lib/dolly/mango.rb +156 -0
- data/lib/dolly/mango_index.rb +73 -0
- data/lib/dolly/properties.rb +36 -0
- data/lib/dolly/property.rb +58 -47
- data/lib/dolly/property_manager.rb +53 -0
- data/lib/dolly/property_set.rb +23 -0
- data/lib/dolly/query.rb +63 -75
- data/lib/dolly/query_arguments.rb +35 -0
- data/lib/dolly/request.rb +12 -105
- data/lib/dolly/request_header.rb +26 -0
- data/lib/dolly/timestamp.rb +24 -0
- data/lib/dolly/version.rb +1 -1
- data/lib/dolly/view_query.rb +21 -0
- data/lib/{dolly → railties}/railtie.rb +2 -1
- data/lib/refinements/hash_refinements.rb +27 -0
- data/lib/refinements/string_refinements.rb +28 -0
- data/lib/tasks/db.rake +27 -4
- data/test/bulk_document_test.rb +8 -5
- data/test/document_test.rb +132 -95
- data/test/document_type_test.rb +28 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/log/test.log +55435 -33484
- data/test/inheritance_test.rb +23 -0
- data/test/mango_index_test.rb +64 -0
- data/test/mango_test.rb +273 -0
- data/test/property_manager_test.rb +18 -0
- data/test/test_helper.rb +63 -18
- data/test/view_query_test.rb +27 -0
- metadata +66 -138
- data/Rakefile +0 -11
- data/lib/dolly/bulk_error.rb +0 -16
- data/lib/dolly/db_config.rb +0 -20
- data/lib/dolly/interpreter.rb +0 -5
- data/lib/dolly/logger.rb +0 -9
- data/lib/dolly/name_space.rb +0 -28
- data/lib/dolly/timestamps.rb +0 -21
- data/lib/exceptions/dolly.rb +0 -38
- data/test/collection_test.rb +0 -59
- data/test/configuration_test.rb +0 -9
- data/test/dummy/README.rdoc +0 -28
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/javascripts/application.js +0 -13
- data/test/dummy/app/assets/stylesheets/application.css +0 -13
- data/test/dummy/app/controllers/application_controller.rb +0 -5
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -27
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/couchdb.yml +0 -13
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -29
- data/test/dummy/config/environments/production.rb +0 -80
- data/test/dummy/config/environments/test.rb +0 -36
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/secret_token.rb +0 -12
- data/test/dummy/config/initializers/session_store.rb +0 -3
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -23
- data/test/dummy/config/routes.rb +0 -56
- data/test/dummy/lib/couch_rest_adapter/railtie.rb +0 -10
- data/test/dummy/public/404.html +0 -58
- data/test/dummy/public/422.html +0 -58
- data/test/dummy/public/500.html +0 -57
- data/test/dummy/public/favicon.ico +0 -0
- data/test/factories/factories.rb +0 -8
@@ -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,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
|
data/lib/dolly/mango.rb
ADDED
@@ -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
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'dolly/document'
|
5
|
+
|
6
|
+
module Dolly
|
7
|
+
class MangoIndex
|
8
|
+
class << self
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
ALL_DOCS = '_all_docs'
|
12
|
+
DESIGN = '_index'
|
13
|
+
ROWS_KEY = :rows
|
14
|
+
DESIGN_PREFIX = '_design/'
|
15
|
+
|
16
|
+
def_delegators :connection, :get, :post
|
17
|
+
|
18
|
+
def all
|
19
|
+
get(DESIGN)[:indexes]
|
20
|
+
end
|
21
|
+
|
22
|
+
def create(name, fields, type = 'json')
|
23
|
+
post(DESIGN, build_index_structure(name, fields, type))
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_in_database(database, name, fields, type = 'json')
|
27
|
+
connection_for_database(database).post(DESIGN, build_index_structure(name, fields, type))
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_by_fields(fields)
|
31
|
+
rows = get(ALL_DOCS, key: key_from_fields(fields))[ROWS_KEY]
|
32
|
+
(rows && rows.any?)
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_all
|
36
|
+
all.each do |index_doc|
|
37
|
+
next if index_doc[:ddoc].nil?
|
38
|
+
delete(index_doc)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete(index_doc)
|
43
|
+
resource = "#{DESIGN}/#{index_doc[:ddoc]}/json/#{index_doc[:name]}"
|
44
|
+
connection.delete(resource, escape: false)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def connection_for_database(database)
|
50
|
+
Dolly::Connection.new(database.to_sym, Rails.env || :development)
|
51
|
+
end
|
52
|
+
|
53
|
+
def connection
|
54
|
+
@connection ||= Dolly::Document.connection
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_index_structure(name, fields, type)
|
58
|
+
{
|
59
|
+
ddoc: key_from_fields(fields).gsub(DESIGN_PREFIX, ''),
|
60
|
+
index: {
|
61
|
+
fields: fields
|
62
|
+
},
|
63
|
+
name: name,
|
64
|
+
type: type
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def key_from_fields(fields)
|
69
|
+
"#{DESIGN_PREFIX}index_#{fields.join('_')}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'dolly/property_set'
|
2
|
+
require 'dolly/property'
|
3
|
+
|
4
|
+
module Dolly
|
5
|
+
module Properties
|
6
|
+
SPECIAL_KEYS = %i[_id _rev]
|
7
|
+
|
8
|
+
def property *opts, class_name: nil, default: nil
|
9
|
+
opts.each do |opt|
|
10
|
+
|
11
|
+
properties << (prop = Property.new(opt, class_name, default))
|
12
|
+
send(:attr_reader, opt)
|
13
|
+
|
14
|
+
define_method(:"#{opt}=") { |value| write_attribute(opt, value) }
|
15
|
+
define_method(:"#{opt}?") { send(opt) } if prop.boolean?
|
16
|
+
define_method(:"[]") {|name| send(name) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def properties
|
21
|
+
@properties ||= PropertySet.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def all_property_keys
|
25
|
+
properties.map(&:key) + SPECIAL_KEYS
|
26
|
+
end
|
27
|
+
|
28
|
+
def property_keys
|
29
|
+
all_property_keys - SPECIAL_KEYS
|
30
|
+
end
|
31
|
+
|
32
|
+
def property_clean_doc(doc)
|
33
|
+
doc.reject { |key, _value| !property_keys.include?(key) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/dolly/property.rb
CHANGED
@@ -1,85 +1,96 @@
|
|
1
|
+
require 'refinements/string_refinements'
|
2
|
+
|
1
3
|
module Dolly
|
2
4
|
class Property
|
3
|
-
|
4
|
-
|
5
|
-
attr_reader :class_name, :default
|
5
|
+
attr_reader :key, :class_name, :default
|
6
|
+
CANT_CLONE = [NilClass, TrueClass, FalseClass, Integer]
|
6
7
|
|
7
|
-
|
8
|
+
using StringRefinements
|
8
9
|
|
9
|
-
def initialize
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@default = @default.clone if @default && CANT_CLONE.none? { |klass| @default.is_a? klass }
|
14
|
-
@value = @default if @default
|
15
|
-
warn 'There are some unprocessed options!' if opts.present?
|
10
|
+
def initialize(key, class_name, default = nil)
|
11
|
+
@key = key
|
12
|
+
@default = default
|
13
|
+
@class_name = class_name
|
16
14
|
end
|
17
15
|
|
18
|
-
def value
|
19
|
-
|
20
|
-
return
|
21
|
-
return
|
22
|
-
|
23
|
-
|
16
|
+
def cast_value(value)
|
17
|
+
return set_default if value.nil?
|
18
|
+
return value unless class_name
|
19
|
+
return self_klass.new(value) unless respond_to?(klass_sym)
|
20
|
+
send(klass_sym, value)
|
21
|
+
end
|
24
22
|
|
25
|
-
|
23
|
+
def boolean?
|
24
|
+
[TrueClass, FalseClass].include?(class_name)
|
25
|
+
end
|
26
26
|
|
27
|
-
|
27
|
+
def string_value(value)
|
28
|
+
value.to_s
|
28
29
|
end
|
29
30
|
|
30
|
-
def
|
31
|
-
|
31
|
+
def hash_value(value)
|
32
|
+
value.to_h
|
32
33
|
end
|
33
34
|
|
34
|
-
def
|
35
|
-
|
35
|
+
def integer_value(value)
|
36
|
+
value.to_i
|
36
37
|
end
|
37
38
|
|
38
|
-
def
|
39
|
-
|
39
|
+
def float_value(value)
|
40
|
+
value.to_f
|
40
41
|
end
|
41
42
|
|
42
|
-
def
|
43
|
-
|
43
|
+
def date_value(value)
|
44
|
+
return value.to_date if value.respond_to?(:to_date)
|
45
|
+
Date.parse(value)
|
44
46
|
end
|
45
47
|
|
46
|
-
def
|
47
|
-
|
48
|
+
def time_value(value)
|
49
|
+
return value.to_time if value.respond_to?(:to_time)
|
50
|
+
DateTime.parse(value).to_time
|
48
51
|
end
|
49
52
|
|
50
|
-
def
|
51
|
-
|
53
|
+
def date_time_value(value)
|
54
|
+
return value.to_datetime if value.respond_to?(:to_datetime)
|
55
|
+
DateTime.parse(value)
|
52
56
|
end
|
53
57
|
|
54
|
-
def
|
55
|
-
|
58
|
+
def true_class_value(value)
|
59
|
+
truthy_value?(value)
|
56
60
|
end
|
57
61
|
|
58
|
-
def
|
59
|
-
|
62
|
+
def false_class_value(value)
|
63
|
+
truthy_value?(value)
|
60
64
|
end
|
61
65
|
|
62
|
-
|
63
|
-
|
66
|
+
private
|
67
|
+
|
68
|
+
def truthy_value?(value)
|
69
|
+
value =~ /true/ || value === true
|
64
70
|
end
|
65
71
|
|
66
|
-
def
|
67
|
-
|
72
|
+
def klass_sym
|
73
|
+
:"#{self_klass.name.underscore}_value"
|
68
74
|
end
|
69
75
|
|
70
|
-
def
|
71
|
-
|
76
|
+
def self_klass
|
77
|
+
return unless class_name
|
78
|
+
return class_name if class_name.is_a?(Class)
|
79
|
+
Object.const_get class_name
|
72
80
|
end
|
73
81
|
|
74
|
-
|
75
|
-
|
76
|
-
|
82
|
+
def set_default
|
83
|
+
return unless default_present?
|
84
|
+
return default unless cant_clone_default?
|
85
|
+
default.clone
|
77
86
|
end
|
78
87
|
|
79
|
-
def
|
80
|
-
|
81
|
-
@class_name.is_a?(Class)? @class_name : @class_name.constantize
|
88
|
+
def cant_clone_default?
|
89
|
+
CANT_CLONE.none? { |klass| default.is_a? klass }
|
82
90
|
end
|
83
91
|
|
92
|
+
def default_present?
|
93
|
+
!default.nil?
|
94
|
+
end
|
84
95
|
end
|
85
96
|
end
|