dolly 3.0.0 → 3.1.2

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