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 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