dolly 1.1.5 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|