dolly 3.0.0 → 3.1.2

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: 2242b051913632959c785357a5de12506673ae26c369bdaccb77763edcf5a15b
4
+ data.tar.gz: db0e1cb72dba11f0adc1184338754266dfccfe2eabd4188a23b1a313d236f2bf
5
5
  SHA512:
6
- metadata.gz: a4a534952347905e024d8da5eec35a31220305edb10528e2d8e30608691ab42143a09c08a005f0121c6acda6a7f6c53320907eb9c81f4b821966d1980c6040d8
7
- data.tar.gz: b4f1d1f5994aa3d963d2fe77f2c8e4b67e4d657c66a4da5548924630da64d28a70f0902b9bd503c08c4a982a47a34e188a0468ee2559eab8a0c1599ede6881c1
6
+ metadata.gz: '096b5264f6a0c36dc68c5c6179300fc7ca2b7fb9d8806b83929ae34180d0f900b8fb64053a6423b51d9d12b716d685cc580618290f5b80cb9041decaa217af1d'
7
+ data.tar.gz: 1220024efbc8a6bb491099f612b260f010d74bbf5d26bc96367ec676cb1095ecbf3f98194b1449f2563116770f23f5ba9bbac985a0b314ba18f3658ee85b5278
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
+ ```
@@ -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)
@@ -1,3 +1,4 @@
1
+ require 'curb'
1
2
  require 'oj'
2
3
  require 'cgi'
3
4
  require 'net/http'
@@ -5,24 +6,28 @@ require 'dolly/request_header'
5
6
  require 'dolly/exceptions'
6
7
  require 'dolly/configuration'
7
8
  require 'refinements/string_refinements'
9
+ require 'dolly/framework_helper'
8
10
 
9
11
  module Dolly
10
12
  class Connection
11
13
  include Dolly::Configuration
14
+ include Dolly::FrameworkHelper
12
15
  attr_reader :db, :app_env
13
16
 
14
- DEFAULT_HEADER = { 'Content-Type' => 'application/json' }
17
+ DEFAULT_HEADER = { 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
18
+ SECURE_PROTOCOL = 'https'
19
+ DEFAULT_DATABASE = :default
15
20
 
16
21
  using StringRefinements
17
22
 
18
- def initialize db = :default, app_env = :development
23
+ def initialize db = DEFAULT_DATABASE, app_env = :development
19
24
  @db = db
20
25
  @app_env = app_env
21
26
  end
22
27
 
23
28
  def get(resource, data = {})
24
29
  query = { query: values_to_json(data) } if data
25
- request :get, resource.cgi_escape, query
30
+ request :get, resource, query
26
31
  end
27
32
 
28
33
  def post resource, data
@@ -33,8 +38,10 @@ module Dolly
33
38
  request :put, resource.cgi_escape, data
34
39
  end
35
40
 
36
- def delete resource, rev
37
- request :delete, resource.cgi_escape, query: { rev: rev }
41
+ def delete resource, rev = nil, escape: true
42
+ query = "?rev=#{rev}" if rev
43
+ resource = "#{escape ? resource.cgi_escape : resource}#{query}"
44
+ request :delete, resource
38
45
  end
39
46
 
40
47
  def view resource, opts
@@ -58,46 +65,55 @@ module Dolly
58
65
  end
59
66
 
60
67
  def request(method, resource, data = {})
61
- headers = Dolly::HeaderRequest.new data.delete(:headers)
62
- uri = build_uri(resource, data.delete(:query))
63
- klass = request_method(method)
64
- req = klass.new(uri, headers)
65
- req.body = format_data(data, headers.json?)
66
- response = start_request(req)
67
-
68
- response_format(response)
68
+ db_resource = (resource =~ %r{^/}) ? resource : "/#{db_name}/#{resource}"
69
+ headers = fetch_headers(data)
70
+ body = fetch_body(data)
71
+ uri = URI("#{base_uri}#{db_resource}")
72
+
73
+ conn = curl_method_call(method, uri, body) do |curl|
74
+ if env['username'].present?
75
+ curl.http_auth_types = :basic
76
+ curl.username = env['username']
77
+ curl.password = env['password'].to_s
78
+ end
79
+
80
+ headers.each { |k, v| curl.headers[k] = v } if headers.present?
81
+ end
82
+ response_format(conn, method)
69
83
  end
70
84
 
71
85
  private
72
86
 
73
- 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
87
+ def fetch_headers(data)
88
+ return unless data.is_a?(Hash)
89
+ Dolly::HeaderRequest.new(data&.delete(:headers))
78
90
  end
79
91
 
80
- def response_format(res)
81
- raise Dolly::ResourceNotFound if res.code.to_i == 404
82
- raise Dolly::ServerError.new(res.body) if (400..600).include? res.code.to_i
83
- Oj.load(res.body, symbol_keys: true)
84
- end
92
+ def fetch_body(data)
93
+ return data unless data.is_a?(Hash)
85
94
 
86
- def format_data(data = nil, is_json)
87
- return unless data
88
- body = data.delete(:_body) || data
89
- is_json ? body.to_json : body
95
+ data&.delete(:headers)
96
+ data&.merge!(data&.delete(:query) || {})
90
97
  end
91
98
 
92
- def build_uri(resource, query = nil)
93
- query_str = "?#{to_query(query)}" if query
94
- uri = (resource =~ %r{^/}) ? resource : "/#{db_name}/#{resource}"
95
-
96
- URI("#{base_uri}#{uri}#{query_str}")
99
+ def curl_method_call(method, uri, data, &block)
100
+ return Curl::Easy.http_head(uri.to_s, &block) if method.to_sym == :head
101
+ return Curl.delete(uri.to_s, &block) if method.to_sym == :delete
102
+ return Curl.send(method, uri, data, &block) if method.to_sym == :get
103
+ Curl.send(method, uri.to_s, data.to_json, &block)
97
104
  end
98
105
 
99
- def request_method(method_name)
100
- Object.const_get("Net::HTTP::#{method_name.capitalize}")
106
+ def response_format(res, method)
107
+ raise Dolly::ResourceNotFound if res.status.to_i == 404
108
+ raise Dolly::ServerError.new(res.status.to_i) if (400..600).include? res.status.to_i
109
+ return res.header_str if method == :head
110
+
111
+ data = Oj.load(res.body_str, symbol_keys: true)
112
+ return data unless rails?
113
+ return data.with_indifferent_access if data.is_a?(Hash)
114
+ data
115
+ rescue Oj::ParseError
116
+ res.body_str
101
117
  end
102
118
 
103
119
  def values_to_json hash
@@ -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'
@@ -11,15 +15,20 @@ require 'dolly/timestamp'
11
15
  require 'dolly/query_arguments'
12
16
  require 'dolly/document_creation'
13
17
  require 'dolly/class_methods_delegation'
18
+ require 'dolly/framework_helper'
14
19
  require 'refinements/string_refinements'
15
20
 
16
21
  module Dolly
17
22
  class Document
23
+ extend Mango
18
24
  extend Query
25
+ extend ViewQuery
19
26
  extend Request
20
27
  extend DepracatedDatabase
21
28
  extend Properties
22
29
  extend DocumentCreation
30
+
31
+ include DocumentType
23
32
  include PropertyManager
24
33
  include Timestamp
25
34
  include DocumentState
@@ -27,17 +36,35 @@ module Dolly
27
36
  include Attachment
28
37
  include QueryArguments
29
38
  include ClassMethodsDelegation
39
+ include FrameworkHelper
30
40
 
31
41
  attr_writer :doc
32
42
 
33
- def initialize attributes = {}
43
+ def initialize(attributes = {})
44
+ init_ancestor_properties
34
45
  properties.each(&build_property(attributes))
35
46
  end
36
47
 
37
48
  protected
38
49
 
39
50
  def doc
40
- @doc ||= {}
51
+ @doc ||= doc_for_framework
52
+ end
53
+
54
+ def init_ancestor_properties
55
+ self.class.ancestors.map do |ancestor|
56
+ begin
57
+ ancestor.properties.entries.each do |property|
58
+ properties << property
59
+ end
60
+ rescue NoMethodError => e
61
+ end
62
+ end
63
+ end
64
+
65
+ def doc_for_framework
66
+ return {} unless rails?
67
+ {}.with_indifferent_access
41
68
  end
42
69
  end
43
70
  end
@@ -1,16 +1,23 @@
1
1
  require 'dolly/properties'
2
+ require 'dolly/framework_helper'
2
3
 
3
4
  module Dolly
4
5
  module DocumentCreation
5
6
  include Properties
7
+ include FrameworkHelper
6
8
 
7
9
  def from_doc(doc)
8
10
  attributes = property_clean_doc(doc)
9
- new(attributes).tap { |model| model.doc = doc }
11
+
12
+ new(attributes).tap do |model|
13
+ model.send(:doc).merge!(doc)
14
+ end
10
15
  end
11
16
 
12
17
  def from_json(json)
13
- from_doc(Oj.load(json, symbol_keys: true))
18
+ raw_data = Oj.load(json, symbol_keys: true)
19
+ data = rails? ? data.with_indifferent_access : raw_data
20
+ from_doc(data)
14
21
  end
15
22
 
16
23
  def create(attributes)
@@ -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,29 @@ 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 BulkError < RuntimeError
29
+ def intialize(error:, reason:, obj:)
30
+ @error = error
31
+ @reason = reason
32
+ @obj = obj
33
+ end
34
+
35
+ def to_s
36
+ "#{@error} on #{@obj} because #{@reason}."
37
+ end
38
+ end
39
+
40
+ class IndexNotFoundError < RuntimeError; end
18
41
  class InvalidConfigFileError < RuntimeError; end
19
42
  class InvalidProperty < RuntimeError; end
20
43
  class DocumentInvalidError < RuntimeError; end
@@ -0,0 +1,7 @@
1
+ module Dolly
2
+ module FrameworkHelper
3
+ def rails?
4
+ defined?(ActiveSupport::HashWithIndifferentAccess)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'refinements/hash_refinements'
4
+
5
+ module Dolly
6
+ module Mango
7
+ using HashRefinements
8
+
9
+ SELECTOR_SYMBOL = '$'
10
+
11
+ COMBINATION_OPERATORS = %I[
12
+ and
13
+ or
14
+ not
15
+ nor
16
+ all
17
+ elemMatch
18
+ allMath
19
+ ].freeze
20
+
21
+ CONDITION_OPERATORS = %I[
22
+ lt
23
+ lte
24
+ eq
25
+ ne
26
+ gte
27
+ gt
28
+ exists
29
+ in
30
+ nin
31
+ size
32
+ mod
33
+ regex
34
+ ].freeze
35
+
36
+ TYPE_OPERATOR = %I[
37
+ type
38
+ $type
39
+ ]
40
+
41
+ ALL_OPERATORS = COMBINATION_OPERATORS + CONDITION_OPERATORS
42
+
43
+ DESIGN = '_find'
44
+
45
+ def find_by(query, opts = {})
46
+ build_model_from_doc(find_doc_by(query, opts))
47
+ end
48
+
49
+ def find_doc_by(query, opts = {})
50
+ opts.merge!(limit: 1)
51
+ response = perform_query(build_query(query, opts))
52
+ print_index_warning(query) if response.fetch(:warning, nil)
53
+ response[:docs].first
54
+ end
55
+
56
+ def where(query, opts = {})
57
+ docs_where(query, opts).map do |doc|
58
+ build_model_from_doc(doc)
59
+ end
60
+ end
61
+
62
+ def docs_where(query, opts = {})
63
+ response = perform_query(build_query(query, opts))
64
+ print_index_warning(query) if response.fetch(:warning, nil)
65
+ response[:docs]
66
+ end
67
+
68
+ def find_bare(id, fields, options = {})
69
+ q = { _id: id }
70
+ opts = { fields: fields }.merge(options)
71
+ query = build_query(q, opts)
72
+ response = perform_query(query)
73
+ response[:docs]
74
+ end
75
+
76
+ def where_bare(selector, fields, options = {})
77
+ opts = { fields: fields }.merge(options)
78
+ query = build_query(selector, opts)
79
+ response = perform_query(query)
80
+ response[:docs]
81
+ end
82
+
83
+ def find_with_metadata(query, options = {})
84
+ opts = options.merge!(limit: 1)
85
+ perform_query(build_query(query, opts))
86
+ end
87
+
88
+ def where_with_metadata(query, options = {})
89
+ perform_query(build_query(query, options))
90
+ end
91
+
92
+ def perform_query(structured_query)
93
+ connection.post(DESIGN, structured_query)
94
+ end
95
+
96
+ private
97
+
98
+ def print_index_warning(query)
99
+ message = "Index not found for #{query.inspect}"
100
+ if (defined?(Rails.logger) && Rails&.env&.development?)
101
+ Rails.logger.info(message)
102
+ else
103
+ puts message
104
+ end
105
+ end
106
+
107
+ def build_model_from_doc(doc)
108
+ return nil if doc.nil?
109
+ new(doc.slice(*all_property_keys)).tap { |d| d.rev = doc[:_rev] }
110
+ end
111
+
112
+ def build_query(query, opts)
113
+ { 'selector' => build_selectors(query) }.merge(opts)
114
+ end
115
+
116
+ def build_selectors(query)
117
+ query.deep_transform_keys do |key|
118
+ next build_key(key) if is_operator?(key)
119
+ next key if is_type_operator?(key)
120
+ raise Dolly::InvalidMangoOperatorError.new(key) unless has_property?(key)
121
+ key
122
+ end
123
+ end
124
+
125
+ def build_key(key)
126
+ return key if key.to_s.starts_with?(SELECTOR_SYMBOL)
127
+ "#{SELECTOR_SYMBOL}#{key}"
128
+ end
129
+
130
+ def is_operator?(key)
131
+ ALL_OPERATORS.include?(key) || key.to_s.starts_with?(SELECTOR_SYMBOL)
132
+ end
133
+
134
+ def fetch_fields(query)
135
+ deep_keys(query).reject do |key|
136
+ is_operator?(key) || is_type_operator?(key)
137
+ end
138
+ end
139
+
140
+ def has_property?(key)
141
+ self.all_property_keys.include?(key)
142
+ end
143
+
144
+ def is_type_operator?(key)
145
+ TYPE_OPERATOR.include?(key.to_sym)
146
+ end
147
+
148
+ def deep_keys(obj)
149
+ case obj
150
+ when Hash then obj.keys + obj.values.flat_map { |v| deep_keys(v) }
151
+ when Array then obj.flat_map { |i| deep_keys(i) }
152
+ else []
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'dolly/document'
5
+
6
+ module Dolly
7
+ class MangoIndex
8
+ class << self
9
+ extend Forwardable
10
+
11
+ ALL_DOCS = '_all_docs'
12
+ DESIGN = '_index'
13
+ ROWS_KEY = :rows
14
+ DESIGN_PREFIX = '_design/'
15
+
16
+ def_delegators :connection, :get, :post
17
+
18
+ def all
19
+ get(DESIGN)[:indexes]
20
+ end
21
+
22
+ def create(name, fields, type = 'json')
23
+ post(DESIGN, build_index_structure(name, fields, type))
24
+ end
25
+
26
+ def create_in_database(database, name, fields, type = 'json')
27
+ connection_for_database(database).post(DESIGN, build_index_structure(name, fields, type))
28
+ end
29
+
30
+ def find_by_fields(fields)
31
+ rows = get(ALL_DOCS, key: key_from_fields(fields))[ROWS_KEY]
32
+ (rows && rows.any?)
33
+ end
34
+
35
+ def delete_all
36
+ all.each do |index_doc|
37
+ next if index_doc[:ddoc].nil?
38
+ delete(index_doc)
39
+ end
40
+ end
41
+
42
+ def delete(index_doc)
43
+ resource = "#{DESIGN}/#{index_doc[:ddoc]}/json/#{index_doc[:name]}"
44
+ connection.delete(resource, escape: false)
45
+ end
46
+
47
+ private
48
+
49
+ def connection_for_database(database)
50
+ Dolly::Connection.new(database.to_sym, Rails.env || :development)
51
+ end
52
+
53
+ def connection
54
+ @connection ||= Dolly::Document.connection
55
+ end
56
+
57
+ def build_index_structure(name, fields, type)
58
+ {
59
+ ddoc: key_from_fields(fields).gsub(DESIGN_PREFIX, ''),
60
+ index: {
61
+ fields: fields
62
+ },
63
+ name: name,
64
+ type: type
65
+ }
66
+ end
67
+
68
+ def key_from_fields(fields)
69
+ "#{DESIGN_PREFIX}index_#{fields.join('_')}"
70
+ end
71
+ end
72
+ end
73
+ end
@@ -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,12 +21,16 @@ 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)
28
- doc.reject { |key, _value| !property_keys.include?(key) }
33
+ doc.reject { |key, _value| property_keys.exclude?(key.to_sym) }
29
34
  end
30
35
  end
31
36
  end