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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c26d50611f8efd7f5cd7b973256ddb6d885a747a
4
- data.tar.gz: 4c16a1b2b21b55eeba0a9b7c0307dce20af3be90
2
+ SHA256:
3
+ metadata.gz: 52fcc7a3f0888239aaff1a6398a81112d031722db14d3070d83d574179414197
4
+ data.tar.gz: 20d5efb134448842c4c547d85f0f395993fdd8817898e7b174e3e5f5714a7022
5
5
  SHA512:
6
- metadata.gz: a4a534952347905e024d8da5eec35a31220305edb10528e2d8e30608691ab42143a09c08a005f0121c6acda6a7f6c53320907eb9c81f4b821966d1980c6040d8
7
- data.tar.gz: b4f1d1f5994aa3d963d2fe77f2c8e4b67e4d657c66a4da5548924630da64d28a70f0902b9bd503c08c4a982a47a34e188a0468ee2559eab8a0c1599ede6881c1
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
+ ```
@@ -1,5 +1,6 @@
1
1
  require "dolly/version"
2
2
  require "dolly/document"
3
+ require 'dolly/mango_index'
3
4
  require 'railties/railtie' if defined?(Rails)
4
5
 
5
6
  module Dolly
@@ -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 #{doc['id']} on bulk doc was not found in payload.", obj: nil)
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
@@ -1,12 +1,12 @@
1
1
  module Dolly
2
2
  class Collection < DelegateClass(Array)
3
- attr_reader :info
3
+ attr_reader :options
4
4
 
5
- def initialize(rows: [], **info)
6
- @info = info
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 doc_type(row[:id])
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 doc_type(key)
32
- key.match(%r{^([^/]+)/})[1].split('_').collect(&:capitalize).join
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)
@@ -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
- request :delete, resource.cgi_escape, query: { rev: rev }
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.delete(:headers)
62
- uri = build_uri(resource, data.delete(:query))
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
- Net::HTTP.start(req.uri.hostname, req.uri.port) do |http|
75
- req.basic_auth env['username'], env['password'] if env['username'].present?
76
- http.request(req)
77
- end
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
 
@@ -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 attributes = {}
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
@@ -6,6 +6,7 @@ module Dolly
6
6
 
7
7
  def save(options = {})
8
8
  return false unless options[:validate] == false || valid?
9
+ set_type if typed? && type.nil?
9
10
  write_timestamps(persisted?)
10
11
  after_save(connection.put(id, doc))
11
12
  end
@@ -13,8 +13,8 @@ module Dolly
13
13
  "#{name_paramitized}/#{key}"
14
14
  end
15
15
 
16
- def base_id(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
@@ -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
@@ -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
@@ -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
- properties.map(&:key) - SPECIAL_KEYS
29
+ all_property_keys - SPECIAL_KEYS
25
30
  end
26
31
 
27
32
  def property_clean_doc(doc)
@@ -4,6 +4,11 @@ module Dolly
4
4
  keys.include?(key)
5
5
  end
6
6
 
7
+ def <<(property)
8
+ return if include?(property.key)
9
+ super(property)
10
+ end
11
+
7
12
  def [](key)
8
13
  return detect {|property| property.key == key } if key.is_a?(Symbol)
9
14
  super
@@ -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
@@ -1,7 +1,7 @@
1
1
  module Dolly
2
2
  module Request
3
3
  def set_namespace name
4
- @namspace = name
4
+ @namespace = name
5
5
  end
6
6
 
7
7
  def set_app_env env
@@ -1,3 +1,3 @@
1
1
  module Dolly
2
- VERSION = "3.0.0"
2
+ VERSION = "3.0.1"
3
3
  end
@@ -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
@@ -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('_rev')
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
 
@@ -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
@@ -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
@@ -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.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: 2019-06-20 00:00:00.000000000 Z
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
- rubyforge_project:
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