dolly 3.0.0 → 3.0.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.
- checksums.yaml +5 -5
- data/README.md +43 -0
- data/lib/dolly.rb +1 -0
- data/lib/dolly/bulk_document.rb +1 -1
- data/lib/dolly/collection.rb +17 -7
- data/lib/dolly/connection.rb +20 -10
- data/lib/dolly/document.rb +21 -1
- data/lib/dolly/document_state.rb +1 -0
- data/lib/dolly/document_type.rb +21 -2
- data/lib/dolly/exceptions.rb +11 -0
- data/lib/dolly/mango.rb +124 -0
- data/lib/dolly/mango_index.rb +65 -0
- data/lib/dolly/properties.rb +6 -1
- data/lib/dolly/property_set.rb +5 -0
- data/lib/dolly/query.rb +6 -8
- data/lib/dolly/request.rb +1 -1
- data/lib/dolly/version.rb +1 -1
- data/lib/dolly/view_query.rb +14 -0
- data/lib/refinements/hash_refinements.rb +27 -0
- data/lib/tasks/db.rake +16 -1
- data/test/document_test.rb +0 -49
- data/test/document_type_test.rb +28 -0
- data/test/inheritance_test.rb +23 -0
- data/test/mango_index_test.rb +64 -0
- data/test/mango_test.rb +273 -0
- data/test/test_helper.rb +51 -0
- data/test/view_query_test.rb +27 -0
- metadata +17 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 52fcc7a3f0888239aaff1a6398a81112d031722db14d3070d83d574179414197
|
4
|
+
data.tar.gz: 20d5efb134448842c4c547d85f0f395993fdd8817898e7b174e3e5f5714a7022
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f684874eaa87448a0ae67fddf5df98f3e86c5c6a53f8fb7b75e1331d16d63f07e6ef8eb2db17e461609433ccaf770a1c3a3c27b1551b5cf7c823f2d010005075
|
7
|
+
data.tar.gz: f44fa8ea168a1d6ca31042c4eed5b8cf0fdb1fd11dfcc5fb76e177598cce96512e6cca8a842e6c646fa4f65f8e4e0ea9759379ad73954c42fb1694c9803d42c3
|
data/README.md
CHANGED
@@ -33,3 +33,46 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
33
33
|
## Contributing
|
34
34
|
|
35
35
|
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dolly3.
|
36
|
+
|
37
|
+
|
38
|
+
## Migrating from couch 1.x to 2.x
|
39
|
+
|
40
|
+
[Official docs](https://docs.couchdb.org/en/2.3.1/install/index.html)
|
41
|
+
|
42
|
+
|
43
|
+
You will need to uninstall the couchdb service with brew
|
44
|
+
|
45
|
+
`brew services stop couchdb`
|
46
|
+
`brew services uninstall couchdb`
|
47
|
+
|
48
|
+
Download the application from the following source
|
49
|
+
|
50
|
+
http://couchdb.apache.org/#download
|
51
|
+
|
52
|
+
launch fauxton and check your installation
|
53
|
+
|
54
|
+
Copy [this file](https://github.com/apache/couchdb/blob/master/rel/overlay/bin/couchup) into your filesystem
|
55
|
+
|
56
|
+
|
57
|
+
make it executable
|
58
|
+
|
59
|
+
`chmod +x couchup.py`
|
60
|
+
|
61
|
+
and run it
|
62
|
+
|
63
|
+
`./couchup.py -h`
|
64
|
+
|
65
|
+
You might need to install python 3 and pip3 and the following libs
|
66
|
+
|
67
|
+
`pip3 install requests progressbar2`
|
68
|
+
|
69
|
+
move your .couch files into the specified `database_dir` in your [fauxton config](http://127.0.0.1:5984/_utils/#_config/couchdb@localhost)
|
70
|
+
|
71
|
+
|
72
|
+
```
|
73
|
+
$ ./couchup list # Shows your unmigrated 1.x databases
|
74
|
+
$ ./couchup replicate -a # Replicates your 1.x DBs to 2.x
|
75
|
+
$ ./couchup rebuild -a # Optional; starts rebuilding your views
|
76
|
+
$ ./couchup delete -a # Deletes your 1.x DBs (careful!)
|
77
|
+
$ ./couchup list # Should show no remaining databases!
|
78
|
+
```
|
data/lib/dolly.rb
CHANGED
data/lib/dolly/bulk_document.rb
CHANGED
@@ -80,7 +80,7 @@ module Dolly
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def response_error(item)
|
83
|
-
BulkError.new(error: 'Document saved but not local rev updated.', reason: "Document with id #{
|
83
|
+
BulkError.new(error: 'Document saved but not local rev updated.', reason: "Document with id #{item} on bulk doc was not found in payload.", obj: nil)
|
84
84
|
end
|
85
85
|
end
|
86
86
|
end
|
data/lib/dolly/collection.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
module Dolly
|
2
2
|
class Collection < DelegateClass(Array)
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :options
|
4
4
|
|
5
|
-
def initialize(rows: [],
|
6
|
-
@
|
5
|
+
def initialize(rows: [], options: {})
|
6
|
+
@options = options
|
7
7
|
#TODO: We should raise an exception if one of the
|
8
8
|
# requested documents is missing
|
9
|
-
super rows.map(&collect_docs).compact
|
9
|
+
super rows[:rows].map(&collect_docs).compact
|
10
10
|
end
|
11
11
|
|
12
12
|
def first_or_all(forced_first = false)
|
@@ -23,13 +23,23 @@ module Dolly
|
|
23
23
|
def collect_docs
|
24
24
|
lambda do |row|
|
25
25
|
next unless collectable_row?(row)
|
26
|
-
klass = Object.const_get
|
26
|
+
klass = Object.const_get(doc_model(row))
|
27
27
|
klass.from_doc(row[:doc])
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
-
def
|
32
|
-
|
31
|
+
def doc_model(doc)
|
32
|
+
options[:doc_type] || constantize_key(doc[:doc_type]) || constantize_key(doc_type_for(doc[:id]))
|
33
|
+
end
|
34
|
+
|
35
|
+
def doc_type_for(key)
|
36
|
+
return false if key.nil?
|
37
|
+
key.match(%r{^([^/]+)/})[1]
|
38
|
+
end
|
39
|
+
|
40
|
+
def constantize_key(key)
|
41
|
+
return false if key.nil?
|
42
|
+
key.split('_').collect(&:capitalize).join
|
33
43
|
end
|
34
44
|
|
35
45
|
def collectable_row?(row)
|
data/lib/dolly/connection.rb
CHANGED
@@ -12,6 +12,7 @@ module Dolly
|
|
12
12
|
attr_reader :db, :app_env
|
13
13
|
|
14
14
|
DEFAULT_HEADER = { 'Content-Type' => 'application/json' }
|
15
|
+
SECURE_PROTOCOL = 'https'
|
15
16
|
|
16
17
|
using StringRefinements
|
17
18
|
|
@@ -33,8 +34,10 @@ module Dolly
|
|
33
34
|
request :put, resource.cgi_escape, data
|
34
35
|
end
|
35
36
|
|
36
|
-
def delete resource, rev
|
37
|
-
|
37
|
+
def delete resource, rev = nil, escape: true
|
38
|
+
query = { query: { rev: rev } } if rev
|
39
|
+
resource = resource.cgi_escape if escape
|
40
|
+
request :delete, resource, query
|
38
41
|
end
|
39
42
|
|
40
43
|
def view resource, opts
|
@@ -58,28 +61,35 @@ module Dolly
|
|
58
61
|
end
|
59
62
|
|
60
63
|
def request(method, resource, data = {})
|
61
|
-
headers = Dolly::HeaderRequest.new data
|
62
|
-
uri = build_uri(resource, data
|
64
|
+
headers = Dolly::HeaderRequest.new data&.delete(:headers)
|
65
|
+
uri = build_uri(resource, data&.delete(:query))
|
63
66
|
klass = request_method(method)
|
64
67
|
req = klass.new(uri, headers)
|
65
68
|
req.body = format_data(data, headers.json?)
|
66
69
|
response = start_request(req)
|
67
70
|
|
68
|
-
response_format(response)
|
71
|
+
response_format(response, method)
|
69
72
|
end
|
70
73
|
|
71
74
|
private
|
72
75
|
|
73
76
|
def start_request(req)
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
77
|
+
req.basic_auth env['username'], env['password'] if env['username']&.present?
|
78
|
+
|
79
|
+
http = Net::HTTP.new(req.uri.host, req.uri.port)
|
80
|
+
http.use_ssl = secure?
|
81
|
+
|
82
|
+
http.request(req)
|
83
|
+
end
|
84
|
+
|
85
|
+
def secure?
|
86
|
+
env['protocol'] == SECURE_PROTOCOL
|
78
87
|
end
|
79
88
|
|
80
|
-
def response_format(res)
|
89
|
+
def response_format(res, method)
|
81
90
|
raise Dolly::ResourceNotFound if res.code.to_i == 404
|
82
91
|
raise Dolly::ServerError.new(res.body) if (400..600).include? res.code.to_i
|
92
|
+
return res if method == :head
|
83
93
|
Oj.load(res.body, symbol_keys: true)
|
84
94
|
end
|
85
95
|
|
data/lib/dolly/document.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
+
require 'dolly/mango'
|
2
|
+
require 'dolly/mango_index'
|
1
3
|
require 'dolly/query'
|
4
|
+
require 'dolly/view_query'
|
2
5
|
require 'dolly/connection'
|
3
6
|
require 'dolly/request'
|
4
7
|
require 'dolly/depracated_database'
|
5
8
|
require 'dolly/document_state'
|
6
9
|
require 'dolly/properties'
|
10
|
+
require 'dolly/document_type'
|
7
11
|
require 'dolly/identity_properties'
|
8
12
|
require 'dolly/attachment'
|
9
13
|
require 'dolly/property_manager'
|
@@ -15,11 +19,15 @@ require 'refinements/string_refinements'
|
|
15
19
|
|
16
20
|
module Dolly
|
17
21
|
class Document
|
22
|
+
extend Mango
|
18
23
|
extend Query
|
24
|
+
extend ViewQuery
|
19
25
|
extend Request
|
20
26
|
extend DepracatedDatabase
|
21
27
|
extend Properties
|
22
28
|
extend DocumentCreation
|
29
|
+
|
30
|
+
include DocumentType
|
23
31
|
include PropertyManager
|
24
32
|
include Timestamp
|
25
33
|
include DocumentState
|
@@ -30,7 +38,8 @@ module Dolly
|
|
30
38
|
|
31
39
|
attr_writer :doc
|
32
40
|
|
33
|
-
def initialize
|
41
|
+
def initialize(attributes = {})
|
42
|
+
init_ancestor_properties
|
34
43
|
properties.each(&build_property(attributes))
|
35
44
|
end
|
36
45
|
|
@@ -39,5 +48,16 @@ module Dolly
|
|
39
48
|
def doc
|
40
49
|
@doc ||= {}
|
41
50
|
end
|
51
|
+
|
52
|
+
def init_ancestor_properties
|
53
|
+
self.class.ancestors.map do |ancestor|
|
54
|
+
begin
|
55
|
+
ancestor.properties.entries.each do |property|
|
56
|
+
properties << property
|
57
|
+
end
|
58
|
+
rescue NoMethodError => e
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
42
62
|
end
|
43
63
|
end
|
data/lib/dolly/document_state.rb
CHANGED
data/lib/dolly/document_type.rb
CHANGED
@@ -13,8 +13,8 @@ module Dolly
|
|
13
13
|
"#{name_paramitized}/#{key}"
|
14
14
|
end
|
15
15
|
|
16
|
-
def base_id
|
17
|
-
id.sub(%r{^#{name_paramitized}/}, '')
|
16
|
+
def base_id
|
17
|
+
self.id.sub(%r{^#{name_paramitized}/}, '')
|
18
18
|
end
|
19
19
|
|
20
20
|
def name_paramitized
|
@@ -24,5 +24,24 @@ module Dolly
|
|
24
24
|
def class_name
|
25
25
|
is_a?(Class) ? name : self.class.name
|
26
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
|
27
46
|
end
|
28
47
|
end
|
data/lib/dolly/exceptions.rb
CHANGED
@@ -15,6 +15,17 @@ module Dolly
|
|
15
15
|
end
|
16
16
|
end
|
17
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
|
18
29
|
class InvalidConfigFileError < RuntimeError; end
|
19
30
|
class InvalidProperty < RuntimeError; end
|
20
31
|
class DocumentInvalidError < RuntimeError; end
|
data/lib/dolly/mango.rb
ADDED
@@ -0,0 +1,124 @@
|
|
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
|
+
raise Dolly::IndexNotFoundError unless index_exists?(query)
|
51
|
+
opts.merge!(limit: 1)
|
52
|
+
perform_query(build_query(query, opts))[:docs].first
|
53
|
+
end
|
54
|
+
|
55
|
+
def where(query, opts = {})
|
56
|
+
docs_where(query, opts).map do |doc|
|
57
|
+
build_model_from_doc(doc)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def docs_where(query, opts = {})
|
62
|
+
raise Dolly::IndexNotFoundError unless index_exists?(query)
|
63
|
+
perform_query(build_query(query, opts))[:docs]
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def build_model_from_doc(doc)
|
69
|
+
return nil if doc.nil?
|
70
|
+
new(doc.slice(*all_property_keys))
|
71
|
+
end
|
72
|
+
|
73
|
+
def perform_query(structured_query)
|
74
|
+
connection.post(DESIGN, structured_query)
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_query(query, opts)
|
78
|
+
{ 'selector' => build_selectors(query) }.merge(opts)
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_selectors(query)
|
82
|
+
query.deep_transform_keys do |key|
|
83
|
+
next build_key(key) if is_operator?(key)
|
84
|
+
next key if is_type_operator?(key)
|
85
|
+
raise Dolly::InvalidMangoOperatorError.new(key) unless has_property?(key)
|
86
|
+
key
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_key(key)
|
91
|
+
"#{SELECTOR_SYMBOL}#{key}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def is_operator?(key)
|
95
|
+
ALL_OPERATORS.include?(key)
|
96
|
+
end
|
97
|
+
|
98
|
+
def index_exists?(query)
|
99
|
+
Dolly::MangoIndex.find_by_fields(fetch_fields(query))
|
100
|
+
end
|
101
|
+
|
102
|
+
def fetch_fields(query)
|
103
|
+
deep_keys(query).reject do |key|
|
104
|
+
is_operator?(key) || is_type_operator?(key)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def has_property?(key)
|
109
|
+
self.all_property_keys.include?(key)
|
110
|
+
end
|
111
|
+
|
112
|
+
def is_type_operator?(key)
|
113
|
+
TYPE_OPERATOR.include?(key.to_sym)
|
114
|
+
end
|
115
|
+
|
116
|
+
def deep_keys(obj)
|
117
|
+
case obj
|
118
|
+
when Hash then obj.keys + obj.values.flat_map { |v| deep_keys(v) }
|
119
|
+
when Array then obj.flat_map { |i| deep_keys(i) }
|
120
|
+
else []
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,65 @@
|
|
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 find_by_fields(fields)
|
27
|
+
rows = get(ALL_DOCS, key: key_from_fields(fields))[ROWS_KEY]
|
28
|
+
rows && rows.any?
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete_all
|
32
|
+
all.each do |index_doc|
|
33
|
+
next if index_doc[:ddoc].nil?
|
34
|
+
delete(index_doc)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete(index_doc)
|
39
|
+
resource = "#{DESIGN}/#{index_doc[:ddoc]}/json/#{index_doc[:name]}"
|
40
|
+
connection.delete(resource, escape: false)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def connection
|
46
|
+
@connection ||= Dolly::Document.connection
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_index_structure(name, fields, type)
|
50
|
+
{
|
51
|
+
ddoc: key_from_fields(fields).gsub(DESIGN_PREFIX, ''),
|
52
|
+
index: {
|
53
|
+
fields: fields
|
54
|
+
},
|
55
|
+
name: name,
|
56
|
+
type: type
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def key_from_fields(fields)
|
61
|
+
"#{DESIGN_PREFIX}index_#{fields.join('_')}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/dolly/properties.rb
CHANGED
@@ -7,6 +7,7 @@ module Dolly
|
|
7
7
|
|
8
8
|
def property *opts, class_name: nil, default: nil
|
9
9
|
opts.each do |opt|
|
10
|
+
|
10
11
|
properties << (prop = Property.new(opt, class_name, default))
|
11
12
|
send(:attr_reader, opt)
|
12
13
|
|
@@ -20,8 +21,12 @@ module Dolly
|
|
20
21
|
@properties ||= PropertySet.new
|
21
22
|
end
|
22
23
|
|
24
|
+
def all_property_keys
|
25
|
+
properties.map(&:key) + SPECIAL_KEYS
|
26
|
+
end
|
27
|
+
|
23
28
|
def property_keys
|
24
|
-
|
29
|
+
all_property_keys - SPECIAL_KEYS
|
25
30
|
end
|
26
31
|
|
27
32
|
def property_clean_doc(doc)
|
data/lib/dolly/property_set.rb
CHANGED
data/lib/dolly/query.rb
CHANGED
@@ -1,14 +1,17 @@
|
|
1
1
|
require 'dolly/collection'
|
2
2
|
require 'dolly/query_arguments'
|
3
3
|
require 'dolly/document_type'
|
4
|
+
require 'refinements/string_refinements'
|
4
5
|
|
5
6
|
module Dolly
|
6
7
|
module Query
|
7
8
|
include QueryArguments
|
8
9
|
include DocumentType
|
9
10
|
|
11
|
+
using StringRefinements
|
12
|
+
|
10
13
|
def find *keys
|
11
|
-
query_hash = { keys: namespace_keys(keys) }
|
14
|
+
query_hash = { keys: namespace_keys(keys).map { |k| k.cgi_escape } }
|
12
15
|
|
13
16
|
build_collection(query_hash).first_or_all&.itself ||
|
14
17
|
raise(Dolly::ResourceNotFound)
|
@@ -38,16 +41,11 @@ module Dolly
|
|
38
41
|
opts = opts.each_with_object({}) { |(k, v), h| h[k] = escape_value(v) }
|
39
42
|
query_results = raw_view(doc, view_name, opts)
|
40
43
|
|
41
|
-
Collection.new(query_results).first_or_all
|
42
|
-
end
|
43
|
-
|
44
|
-
def raw_view doc, view_name, opts = {}
|
45
|
-
design = "_design/#{doc}/_view/#{view_name}"
|
46
|
-
connection.view(design, opts)
|
44
|
+
Collection.new({ rows: query_results, options: {} }).first_or_all
|
47
45
|
end
|
48
46
|
|
49
47
|
def build_collection(query)
|
50
|
-
Collection.new(connection.get('_all_docs', query.merge(include_docs: true)))
|
48
|
+
Collection.new({ rows: connection.get('_all_docs', query.merge(include_docs: true)), options: { doc_type: self.class_name }})
|
51
49
|
end
|
52
50
|
|
53
51
|
def bulk_document
|
data/lib/dolly/request.rb
CHANGED
data/lib/dolly/version.rb
CHANGED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'dolly/class_methods_delegation'
|
2
|
+
|
3
|
+
module Dolly
|
4
|
+
module ViewQuery
|
5
|
+
def raw_view(doc, view_name, opts = {})
|
6
|
+
design = "_design/#{doc}/_view/#{view_name}"
|
7
|
+
connection.view(design, opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def view_value(doc, view_name, opts = {})
|
11
|
+
raw_view(doc, view_name, opts)[:rows].flat_map { |result| result[:value] }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module HashRefinements
|
2
|
+
refine Hash do
|
3
|
+
# File activesupport/lib/active_support/core_ext/hash/keys.rb, line 82
|
4
|
+
def deep_transform_keys(&block)
|
5
|
+
_deep_transform_keys_in_object(self, &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def slice(*keys)
|
9
|
+
keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def _deep_transform_keys_in_object(object, &block)
|
15
|
+
case object
|
16
|
+
when Hash
|
17
|
+
object.each_with_object({}) do |(key, value), result|
|
18
|
+
result[yield(key)] = _deep_transform_keys_in_object(value, &block)
|
19
|
+
end
|
20
|
+
when Array
|
21
|
+
object.map { |e| _deep_transform_keys_in_object(e, &block) }
|
22
|
+
else
|
23
|
+
object
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/tasks/db.rake
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :db do
|
2
4
|
desc "Will create if missing database and add default views"
|
3
5
|
task setup: :environment do
|
@@ -40,7 +42,7 @@ namespace :db do
|
|
40
42
|
begin
|
41
43
|
hash_doc = Dolly::Document.connection.request(:get, view_doc["_id"])
|
42
44
|
|
43
|
-
rev = hash_doc.delete(
|
45
|
+
rev = hash_doc.delete(:_rev)
|
44
46
|
|
45
47
|
if hash_doc == view_doc
|
46
48
|
puts 'everything up to date'
|
@@ -59,5 +61,18 @@ namespace :db do
|
|
59
61
|
end
|
60
62
|
end
|
61
63
|
|
64
|
+
namespace :index do
|
65
|
+
desc 'Creates indexes for mango querys located in db/indexes/*.json'
|
66
|
+
task create: :environment do
|
67
|
+
indexes_dir = Rails.root.join('db', 'indexes')
|
68
|
+
files = Dir.glob File.join(indexes_dir, '**', '*.json')
|
69
|
+
|
70
|
+
files.each do |file|
|
71
|
+
index_data = JSON.parse(File.read(file))
|
72
|
+
puts "Creating index: #{index_data["name"]}"
|
73
|
+
puts Dolly::MangoIndex.create(index_data['name'], index_data['fields'])
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
62
77
|
end
|
63
78
|
|
data/test/document_test.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
|
3
|
-
class BaseDolly < Dolly::Document; end
|
4
|
-
|
5
3
|
class BarFoo < BaseDolly
|
6
4
|
property :a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :persist
|
7
5
|
end
|
@@ -54,7 +52,6 @@ class Bar < FooBar
|
|
54
52
|
end
|
55
53
|
|
56
54
|
class DocumentTest < Test::Unit::TestCase
|
57
|
-
DB_BASE_PATH = "http://localhost:5984/test".freeze
|
58
55
|
|
59
56
|
def setup
|
60
57
|
data = {foo: 'Foo', bar: 'Bar', type: 'foo_bar'}
|
@@ -578,50 +575,4 @@ class DocumentTest < Test::Unit::TestCase
|
|
578
575
|
assert bar = Bar.new(a: 1)
|
579
576
|
assert_equal 1, bar.a
|
580
577
|
end
|
581
|
-
|
582
|
-
private
|
583
|
-
def generic_response rows, count = 1
|
584
|
-
{total_rows: count, offset:0, rows: rows}
|
585
|
-
end
|
586
|
-
|
587
|
-
def build_view_response properties
|
588
|
-
rows = properties.map.with_index do |v, i|
|
589
|
-
{
|
590
|
-
id: "foo_bar/#{i}",
|
591
|
-
key: "foo_bar",
|
592
|
-
value: 1,
|
593
|
-
doc: {_id: "foo_bar/#{i}", _rev: SecureRandom.hex}.merge!(v)
|
594
|
-
}
|
595
|
-
end
|
596
|
-
generic_response rows, properties.count
|
597
|
-
end
|
598
|
-
|
599
|
-
def build_view_collation_response properties
|
600
|
-
rows = properties.map.with_index do |v, i|
|
601
|
-
id = i.zero? ? "foo_bar/#{i}" : "baz/#{i}"
|
602
|
-
{
|
603
|
-
id: id,
|
604
|
-
key: "foo_bar",
|
605
|
-
value: 1,
|
606
|
-
doc: {_id: id, _rev: SecureRandom.hex}.merge!(v)
|
607
|
-
}
|
608
|
-
end
|
609
|
-
generic_response rows, properties.count
|
610
|
-
end
|
611
|
-
|
612
|
-
|
613
|
-
def build_request keys, body, view_name = 'foo_bar'
|
614
|
-
query = "keys=#{CGI::escape keys.to_s.gsub(' ','')}&" unless keys&.empty?
|
615
|
-
stub_request(:get, "#{query_base_path}?#{query.to_s}include_docs=true").
|
616
|
-
to_return(body: body.to_json)
|
617
|
-
end
|
618
|
-
|
619
|
-
def query_base_path
|
620
|
-
"#{DB_BASE_PATH}/_all_docs"
|
621
|
-
end
|
622
|
-
|
623
|
-
def build_save_request(obj)
|
624
|
-
stub_request(:put, "#{DB_BASE_PATH}/#{CGI.escape(obj.id)}").
|
625
|
-
to_return(body: {ok: true, id: obj.id, rev: "FF0000" }.to_json)
|
626
|
-
end
|
627
578
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class TypedDoc < Dolly::Document
|
4
|
+
typed_model
|
5
|
+
end
|
6
|
+
|
7
|
+
class UntypedDoc < Dolly::Document
|
8
|
+
end
|
9
|
+
|
10
|
+
class DocumentTypeTest < Test::Unit::TestCase
|
11
|
+
test 'typed?' do
|
12
|
+
assert_equal(TypedDoc.new.typed?, true)
|
13
|
+
assert_equal(UntypedDoc.new.typed?, false)
|
14
|
+
end
|
15
|
+
|
16
|
+
test 'typed_model' do
|
17
|
+
assert_equal(TypedDoc.new.type, nil)
|
18
|
+
assert_equal(UntypedDoc.new.respond_to?(:type), false)
|
19
|
+
assert_raise NoMethodError do
|
20
|
+
UntypedDoc.new.type
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'set_type' do
|
25
|
+
assert_equal(TypedDoc.new.set_type, TypedDoc.name_paramitized)
|
26
|
+
assert_equal(UntypedDoc.new.set_type, nil)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BaseDoc < Dolly::Document
|
4
|
+
typed_model
|
5
|
+
end
|
6
|
+
|
7
|
+
class BaseBaseDoc < BaseDoc
|
8
|
+
property :supertype
|
9
|
+
end
|
10
|
+
|
11
|
+
class NewBar < BaseBaseDoc
|
12
|
+
property :a, :b
|
13
|
+
end
|
14
|
+
|
15
|
+
class InheritanceTest < Test::Unit::TestCase
|
16
|
+
test 'property inheritance' do
|
17
|
+
assert_equal(BaseBaseDoc.new.properties.map(&:key), [:supertype, :type])
|
18
|
+
end
|
19
|
+
|
20
|
+
test 'deep properties inheritance' do
|
21
|
+
assert_equal(NewBar.new.properties.map(&:key), [:a, :b, :supertype, :type])
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FooBar < BaseDolly
|
4
|
+
property :foo, :bar
|
5
|
+
property :with_default, default: 1
|
6
|
+
property :boolean, class_name: TrueClass, default: true
|
7
|
+
property :date, class_name: Date
|
8
|
+
property :time, class_name: Time
|
9
|
+
property :datetime, class_name: DateTime
|
10
|
+
property :is_nil, class_name: NilClass, default: nil
|
11
|
+
|
12
|
+
timestamps!
|
13
|
+
end
|
14
|
+
|
15
|
+
class MangoIndexTest < Test::Unit::TestCase
|
16
|
+
DB_BASE_PATH = "http://localhost:5984/test".freeze
|
17
|
+
|
18
|
+
def setup
|
19
|
+
stub_request(:get, index_base_path).
|
20
|
+
to_return(body: { indexes:[ {
|
21
|
+
ddoc: nil,
|
22
|
+
name:"_all_docs",
|
23
|
+
type:"special",
|
24
|
+
def:{ fields:[{ _id:"asc" }] }
|
25
|
+
},
|
26
|
+
{
|
27
|
+
ddoc: "_design/1",
|
28
|
+
name:"foo-index-json",
|
29
|
+
type:"json",
|
30
|
+
def:{ fields:[{ foo:"asc" }] }
|
31
|
+
}
|
32
|
+
]}.to_json)
|
33
|
+
end
|
34
|
+
|
35
|
+
test '#delete_all' do
|
36
|
+
previous_indexes = Dolly::MangoIndex.all
|
37
|
+
|
38
|
+
stub_request(:delete, index_delete_path(previous_indexes.last)).
|
39
|
+
to_return(body: { "ok": true }.to_json)
|
40
|
+
|
41
|
+
Dolly::MangoIndex.delete_all
|
42
|
+
|
43
|
+
stub_request(:get, index_base_path).
|
44
|
+
to_return(body: { indexes:[ {
|
45
|
+
ddoc: nil,
|
46
|
+
name:"_all_docs",
|
47
|
+
type:"special",
|
48
|
+
def:{ fields:[{ _id:"asc" }] }
|
49
|
+
}
|
50
|
+
]}.to_json)
|
51
|
+
|
52
|
+
new_indexes = Dolly::MangoIndex.all
|
53
|
+
assert_not_equal(new_indexes.length, previous_indexes.length)
|
54
|
+
assert_equal(new_indexes.length, 1)
|
55
|
+
end
|
56
|
+
|
57
|
+
def index_base_path
|
58
|
+
"#{DB_BASE_PATH}/_index"
|
59
|
+
end
|
60
|
+
|
61
|
+
def index_delete_path(doc)
|
62
|
+
"#{index_base_path}/#{doc[:ddoc]}/json/#{doc[:name]}"
|
63
|
+
end
|
64
|
+
end
|
data/test/mango_test.rb
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class FooBar < BaseDolly
|
4
|
+
property :foo, :bar
|
5
|
+
property :with_default, default: 1
|
6
|
+
property :boolean, class_name: TrueClass, default: true
|
7
|
+
property :date, class_name: Date
|
8
|
+
property :time, class_name: Time
|
9
|
+
property :datetime, class_name: DateTime
|
10
|
+
property :is_nil, class_name: NilClass, default: nil
|
11
|
+
|
12
|
+
timestamps!
|
13
|
+
end
|
14
|
+
|
15
|
+
class FooBarTyped < BaseDolly
|
16
|
+
typed_model
|
17
|
+
end
|
18
|
+
|
19
|
+
class MangoTest < Test::Unit::TestCase
|
20
|
+
DB_BASE_PATH = "http://localhost:5984/test".freeze
|
21
|
+
|
22
|
+
def setup
|
23
|
+
data = {foo: 'Foo', bar: 'Bar', type: 'foo_bar'}
|
24
|
+
|
25
|
+
all_docs = [ {foo: 'Foo B', bar: 'Bar B', type: 'foo_bar'}, {foo: 'Foo A', bar: 'Bar A', type: 'foo_bar'}]
|
26
|
+
|
27
|
+
view_resp = build_view_response [data]
|
28
|
+
empty_resp = build_view_response []
|
29
|
+
not_found_resp = generic_response [{ key: "foo_bar/2", error: "not_found" }]
|
30
|
+
@multi_resp = build_view_response all_docs
|
31
|
+
@multi_type_resp = build_view_collation_response all_docs
|
32
|
+
|
33
|
+
build_request [["foo_bar","1"]], view_resp
|
34
|
+
build_request [["foo_bar","2"]], empty_resp
|
35
|
+
build_request [["foo_bar","1"],["foo_bar","2"]], @multi_resp
|
36
|
+
|
37
|
+
stub_request(:get, "#{query_base_path}?startkey=%22foo_bar%2F%22&endkey=%22foo_bar%2F%EF%BF%B0%22&include_docs=true").
|
38
|
+
to_return(body: @multi_resp.to_json)
|
39
|
+
|
40
|
+
stub_request(:get, "#{all_docs_path}?key=\"index_foo\"").
|
41
|
+
to_return(body: {
|
42
|
+
total_rows: 2,
|
43
|
+
offset: 0,
|
44
|
+
rows: [{
|
45
|
+
id: '_design/index_foo',
|
46
|
+
key: '_design/index_foo',
|
47
|
+
value: { rev: '1-c5457a0d26da85f15c4ad6bd739e441d' }
|
48
|
+
}]}.to_json)
|
49
|
+
|
50
|
+
stub_request(:get, "#{all_docs_path}?key=\"index_date\"").
|
51
|
+
to_return(body: {
|
52
|
+
total_rows: 2,
|
53
|
+
offset: 0,
|
54
|
+
rows: []}.to_json)
|
55
|
+
end
|
56
|
+
|
57
|
+
test '#find_by' do
|
58
|
+
#TODO: clean up all the fake request creation
|
59
|
+
resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] }
|
60
|
+
|
61
|
+
stub_request(:post, query_base_path).
|
62
|
+
to_return(body: resp.to_json)
|
63
|
+
|
64
|
+
key = 'foo'
|
65
|
+
stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
|
66
|
+
to_return(body: index_response(key).to_json)
|
67
|
+
|
68
|
+
assert_equal(FooBar.find_by(foo: 'bar').class, FooBar)
|
69
|
+
end
|
70
|
+
|
71
|
+
test '#find_by for a property that does not have an index' do
|
72
|
+
#TODO: clean up all the fake request creation
|
73
|
+
resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] }
|
74
|
+
key = 'date'
|
75
|
+
|
76
|
+
stub_request(:post, query_base_path).
|
77
|
+
to_return(body: resp.to_json)
|
78
|
+
|
79
|
+
stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
|
80
|
+
to_return(body: { rows: [] }.to_json)
|
81
|
+
|
82
|
+
assert_raise Dolly::IndexNotFoundError do
|
83
|
+
FooBar.find_by(date: Date.today)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
test '#find_by with no returned data' do
|
88
|
+
resp = { docs: [] }
|
89
|
+
|
90
|
+
stub_request(:post, query_base_path).
|
91
|
+
to_return(body: resp.to_json)
|
92
|
+
|
93
|
+
key = 'foo'
|
94
|
+
stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
|
95
|
+
to_return(body: index_response(key).to_json)
|
96
|
+
|
97
|
+
assert_equal(FooBar.find_by(foo: 'bar'), nil)
|
98
|
+
end
|
99
|
+
|
100
|
+
test '#find_doc_by' do
|
101
|
+
#TODO: clean up all the fake request creation
|
102
|
+
resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] }
|
103
|
+
|
104
|
+
stub_request(:post, query_base_path).
|
105
|
+
to_return(body: resp.to_json)
|
106
|
+
|
107
|
+
key = 'foo'
|
108
|
+
stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
|
109
|
+
to_return(body: index_response(key).to_json)
|
110
|
+
|
111
|
+
assert_equal(FooBar.find_doc_by(foo: 'bar').class, Hash)
|
112
|
+
end
|
113
|
+
|
114
|
+
test '#where' do
|
115
|
+
#TODO: clean up all the fake request creation
|
116
|
+
resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] }
|
117
|
+
|
118
|
+
stub_request(:post, query_base_path).
|
119
|
+
to_return(body: resp.to_json)
|
120
|
+
|
121
|
+
key = 'foo'
|
122
|
+
stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
|
123
|
+
to_return(body: index_response(key).to_json)
|
124
|
+
|
125
|
+
assert_equal(FooBar.where(foo: { eq: 'bar' }).map(&:class).uniq, [FooBar])
|
126
|
+
end
|
127
|
+
|
128
|
+
test '#where for a property that does not have an index' do
|
129
|
+
#TODO: clean up all the fake request creation
|
130
|
+
resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] }
|
131
|
+
|
132
|
+
stub_request(:post, query_base_path).
|
133
|
+
to_return(body: resp.to_json)
|
134
|
+
|
135
|
+
key = 'date'
|
136
|
+
stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
|
137
|
+
to_return(body: { rows: [] }.to_json)
|
138
|
+
|
139
|
+
assert_raise Dolly::IndexNotFoundError do
|
140
|
+
FooBar.where(date: Date.today)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
test '#where with no returned data' do
|
145
|
+
resp = { docs: [] }
|
146
|
+
|
147
|
+
stub_request(:post, query_base_path).
|
148
|
+
to_return(body: resp.to_json)
|
149
|
+
|
150
|
+
key = 'foo'
|
151
|
+
stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
|
152
|
+
to_return(body: index_response(key).to_json)
|
153
|
+
|
154
|
+
assert_equal(FooBar.where(foo: 'bar'), [])
|
155
|
+
end
|
156
|
+
|
157
|
+
test '#docs_where' do
|
158
|
+
#TODO: clean up all the fake request creation
|
159
|
+
resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] }
|
160
|
+
|
161
|
+
stub_request(:post, query_base_path).
|
162
|
+
to_return(body: resp.to_json)
|
163
|
+
|
164
|
+
key = 'foo'
|
165
|
+
stub_request(:get, "#{all_docs_path}?key=\"_design/index_#{key}\"").
|
166
|
+
to_return(body: index_response(key).to_json)
|
167
|
+
|
168
|
+
assert_equal(FooBar.docs_where(foo: { eq: 'bar' }).map(&:class).uniq, [Hash])
|
169
|
+
end
|
170
|
+
|
171
|
+
test '#build_query' do
|
172
|
+
query = { and: [{ _id: { eq: 'foo_bar/1' } } , { foo: { eq: 'bar'}} ] }
|
173
|
+
opts = {}
|
174
|
+
expected = {"selector"=>{"$and"=>[{:_id=>{"$eq"=>"foo_bar/1"}}, {:foo=>{"$eq"=>"bar"}}]}}
|
175
|
+
|
176
|
+
assert_equal(FooBar.send(:build_query, query, opts), expected)
|
177
|
+
end
|
178
|
+
|
179
|
+
test '#build_query with options' do
|
180
|
+
query = { and: [{ _id: { eq: 'foo_bar/1' } } , { foo: { eq: 'bar'}} ] }
|
181
|
+
opts = { limit: 1, fields: ['foo']}
|
182
|
+
expected = {"selector"=>{"$and"=>[{:_id=>{"$eq"=>"foo_bar/1"}}, {:foo=>{"$eq"=>"bar"}}]}, limit: 1, fields: ['foo']}
|
183
|
+
|
184
|
+
assert_equal(FooBar.send(:build_query, query, opts), expected)
|
185
|
+
end
|
186
|
+
|
187
|
+
test '#build_selectors with invalid operator' do
|
188
|
+
query = { _id: { eeeq: 'foo_bar/1' } }
|
189
|
+
|
190
|
+
assert_raise Dolly::InvalidMangoOperatorError do
|
191
|
+
FooBar.send(:build_selectors, query)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
test '#build_selectors with type operator' do
|
196
|
+
query = { _id: { type: "user" } }
|
197
|
+
|
198
|
+
assert_nothing_raised Dolly::InvalidMangoOperatorError do
|
199
|
+
FooBarTyped.send(:build_selectors, query)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
test '#build_selectors with $type operator' do
|
204
|
+
query = { _id: { "$type" => "null" } }
|
205
|
+
|
206
|
+
assert_nothing_raised Dolly::InvalidMangoOperatorError do
|
207
|
+
FooBarTyped.send(:build_selectors, query)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def generic_response rows, count = 1
|
214
|
+
{total_rows: count, offset:0, rows: rows}
|
215
|
+
end
|
216
|
+
|
217
|
+
def build_view_response properties
|
218
|
+
rows = properties.map.with_index do |v, i|
|
219
|
+
{
|
220
|
+
id: "foo_bar/#{i}",
|
221
|
+
key: "foo_bar",
|
222
|
+
value: 1,
|
223
|
+
doc: {_id: "foo_bar/#{i}", _rev: SecureRandom.hex}.merge!(v)
|
224
|
+
}
|
225
|
+
end
|
226
|
+
generic_response rows, properties.count
|
227
|
+
end
|
228
|
+
|
229
|
+
def build_view_collation_response properties
|
230
|
+
rows = properties.map.with_index do |v, i|
|
231
|
+
id = i.zero? ? "foo_bar/#{i}" : "baz/#{i}"
|
232
|
+
{
|
233
|
+
id: id,
|
234
|
+
key: "foo_bar",
|
235
|
+
value: 1,
|
236
|
+
doc: {_id: id, _rev: SecureRandom.hex}.merge!(v)
|
237
|
+
}
|
238
|
+
end
|
239
|
+
generic_response rows, properties.count
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
def build_request keys, body, view_name = 'foo_bar'
|
244
|
+
query = "keys=#{CGI::escape keys.to_s.gsub(' ','')}&" unless keys&.empty?
|
245
|
+
stub_request(:get, "#{query_base_path}?#{query.to_s}include_docs=true").
|
246
|
+
to_return(body: body.to_json)
|
247
|
+
end
|
248
|
+
|
249
|
+
def query_base_path
|
250
|
+
"#{DB_BASE_PATH}/_find"
|
251
|
+
end
|
252
|
+
|
253
|
+
def all_docs_path
|
254
|
+
"#{DB_BASE_PATH}/_all_docs"
|
255
|
+
end
|
256
|
+
|
257
|
+
def build_save_request(obj)
|
258
|
+
stub_request(:put, "#{DB_BASE_PATH}/#{CGI.escape(obj.id)}").
|
259
|
+
to_return(body: {ok: true, id: obj.id, rev: "FF0000" }.to_json)
|
260
|
+
end
|
261
|
+
|
262
|
+
def index_response(key)
|
263
|
+
{
|
264
|
+
rows: [
|
265
|
+
{
|
266
|
+
id: "_design/index_#{key}",
|
267
|
+
key: "_design/index_#{key}",
|
268
|
+
value: { rev: '1-c5457a0d26da85f15c4ad6bd739e441d' }
|
269
|
+
}
|
270
|
+
]
|
271
|
+
}
|
272
|
+
end
|
273
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -13,6 +13,7 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
|
13
13
|
|
14
14
|
class Test::Unit::TestCase
|
15
15
|
DEFAULT_DB = 'test'
|
16
|
+
DB_BASE_PATH = "http://localhost:5984/test".freeze
|
16
17
|
|
17
18
|
setup :global_setup
|
18
19
|
|
@@ -28,4 +29,54 @@ class Test::Unit::TestCase
|
|
28
29
|
def base_path
|
29
30
|
%r{http://.*:5984/#{DEFAULT_DB}}
|
30
31
|
end
|
32
|
+
|
33
|
+
def generic_response rows, count = 1
|
34
|
+
{total_rows: count, offset:0, rows: rows}
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_view_response properties
|
38
|
+
rows = properties.map.with_index do |v, i|
|
39
|
+
{
|
40
|
+
id: "foo_bar/#{i}",
|
41
|
+
key: "foo_bar",
|
42
|
+
value: 1,
|
43
|
+
doc: {_id: "foo_bar/#{i}", _rev: SecureRandom.hex}.merge!(v)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
generic_response rows, properties.count
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_view_collation_response properties
|
50
|
+
rows = properties.map.with_index do |v, i|
|
51
|
+
id = i.zero? ? "foo_bar/#{i}" : "baz/#{i}"
|
52
|
+
{
|
53
|
+
id: id,
|
54
|
+
key: "foo_bar",
|
55
|
+
value: 1,
|
56
|
+
doc_type: id.split("/").first,
|
57
|
+
doc: {
|
58
|
+
_id: id, _rev: SecureRandom.hex
|
59
|
+
}.merge!(v)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
generic_response rows, properties.count
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def build_request keys, body, view_name = 'foo_bar'
|
67
|
+
query = "keys=#{CGI::escape keys.to_s.gsub(' ','')}&" unless keys&.empty?
|
68
|
+
stub_request(:get, "#{query_base_path}?#{query.to_s}include_docs=true").
|
69
|
+
to_return(body: body.to_json)
|
70
|
+
end
|
71
|
+
|
72
|
+
def query_base_path
|
73
|
+
"#{DB_BASE_PATH}/_all_docs"
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_save_request(obj)
|
77
|
+
stub_request(:put, "#{DB_BASE_PATH}/#{CGI.escape(obj.id)}").
|
78
|
+
to_return(body: {ok: true, id: obj.id, rev: "FF0000" }.to_json)
|
79
|
+
end
|
31
80
|
end
|
81
|
+
|
82
|
+
class BaseDolly < Dolly::Document; end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Foo < Dolly::Document
|
4
|
+
end
|
5
|
+
|
6
|
+
class ViewQueryTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def setup
|
9
|
+
all_docs = [ {foo: 'Foo B', bar: 'Bar B', type: 'foo_bar'}, {foo: 'Foo A', bar: 'Bar A', type: 'foo_bar'}]
|
10
|
+
@multi_type_resp = build_view_collation_response all_docs
|
11
|
+
|
12
|
+
stub_request(:get, "http://localhost:5984/test/_design/doc/_view/id?include_docs=true").
|
13
|
+
to_return(body: @multi_type_resp.to_json)
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
test 'raw_view' do
|
18
|
+
assert_equal(Foo.raw_view('doc', 'id'), @multi_type_resp)
|
19
|
+
assert_equal(Foo.raw_view('doc', 'id')[:rows].any?, true)
|
20
|
+
assert_equal(Foo.raw_view('doc', 'id')[:total_rows].nil?, false)
|
21
|
+
end
|
22
|
+
|
23
|
+
test 'view_value' do
|
24
|
+
expected = @multi_type_resp[:rows].flat_map{|res| res[:value]}
|
25
|
+
assert_equal(Foo.view_value('doc', 'id'), expected)
|
26
|
+
end
|
27
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dolly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- javierg
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -116,6 +116,8 @@ files:
|
|
116
116
|
- lib/dolly/document_type.rb
|
117
117
|
- lib/dolly/exceptions.rb
|
118
118
|
- lib/dolly/identity_properties.rb
|
119
|
+
- lib/dolly/mango.rb
|
120
|
+
- lib/dolly/mango_index.rb
|
119
121
|
- lib/dolly/properties.rb
|
120
122
|
- lib/dolly/property.rb
|
121
123
|
- lib/dolly/property_manager.rb
|
@@ -126,15 +128,22 @@ files:
|
|
126
128
|
- lib/dolly/request_header.rb
|
127
129
|
- lib/dolly/timestamp.rb
|
128
130
|
- lib/dolly/version.rb
|
131
|
+
- lib/dolly/view_query.rb
|
129
132
|
- lib/railties/railtie.rb
|
133
|
+
- lib/refinements/hash_refinements.rb
|
130
134
|
- lib/refinements/string_refinements.rb
|
131
135
|
- lib/tasks/db.rake
|
132
136
|
- test/bulk_document_test.rb
|
133
137
|
- test/document_test.rb
|
138
|
+
- test/document_type_test.rb
|
134
139
|
- test/dummy/config/initializers/filter_parameter_logging.rb
|
135
140
|
- test/dummy/log/test.log
|
141
|
+
- test/inheritance_test.rb
|
142
|
+
- test/mango_index_test.rb
|
143
|
+
- test/mango_test.rb
|
136
144
|
- test/support/test.txt
|
137
145
|
- test/test_helper.rb
|
146
|
+
- test/view_query_test.rb
|
138
147
|
homepage: https://www.amco.me
|
139
148
|
licenses: []
|
140
149
|
metadata: {}
|
@@ -153,15 +162,19 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
162
|
- !ruby/object:Gem::Version
|
154
163
|
version: '0'
|
155
164
|
requirements: []
|
156
|
-
|
157
|
-
rubygems_version: 2.6.13
|
165
|
+
rubygems_version: 3.1.4
|
158
166
|
signing_key:
|
159
167
|
specification_version: 4
|
160
168
|
summary: will write something
|
161
169
|
test_files:
|
162
170
|
- test/dummy/config/initializers/filter_parameter_logging.rb
|
163
171
|
- test/dummy/log/test.log
|
172
|
+
- test/inheritance_test.rb
|
173
|
+
- test/document_type_test.rb
|
174
|
+
- test/mango_index_test.rb
|
164
175
|
- test/bulk_document_test.rb
|
176
|
+
- test/view_query_test.rb
|
177
|
+
- test/mango_test.rb
|
165
178
|
- test/support/test.txt
|
166
179
|
- test/test_helper.rb
|
167
180
|
- test/document_test.rb
|