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