diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..c1dad03 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,58 @@ +PATH + remote: . + specs: + dolly (3.0.0) + oj + +GEM + remote: https://rubygems.org/ + specs: + addressable (2.6.0) + public_suffix (>= 2.0.2, < 4.0) + crack (0.4.3) + safe_yaml (~> 1.0.0) + hashdiff (0.4.0) + metaclass (0.0.4) + mocha (1.9.0) + metaclass (~> 0.0.1) + oj (3.7.12) + power_assert (1.1.4) + public_suffix (3.1.0) + rake (10.5.0) + rr (1.2.1) + safe_yaml (1.0.5) + test-unit (3.3.3) + power_assert + test-unit-context (0.5.1) + test-unit (>= 2.4.0) + test-unit-full (0.0.5) + test-unit + test-unit-context + test-unit-notify + test-unit-rr + test-unit-runner-tap + test-unit-notify (1.0.4) + test-unit (>= 2.4.9) + test-unit-rr (1.0.5) + rr (>= 1.1.1) + test-unit (>= 2.5.2) + test-unit-runner-tap (1.1.2) + test-unit + webmock (3.6.0) + addressable (>= 2.3.6) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + +PLATFORMS + ruby + +DEPENDENCIES + bundler (~> 2.0) + dolly! + mocha + rake (~> 10.0) + test-unit-full + webmock + +BUNDLED WITH + 2.0.2 diff --git a/README.md b/README.md index 11c42e3..b3c0a3a 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,46 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dolly3. + + +## Migrating from couch 1.x to 2.x + +[Official docs](https://docs.couchdb.org/en/2.3.1/install/index.html) + + +You will need to uninstall the couchdb service with brew + +`brew services stop couchdb` +`brew services uninstall couchdb` + +Download the application from the following source + +http://couchdb.apache.org/#download + +launch fauxton and check your installation + +Copy [this file](https://github.com/apache/couchdb/blob/master/rel/overlay/bin/couchup) into your filesystem + + +make it executable + +`chmod +x couchup.py` + +and run it + +`./couchup.py -h` + +You might need to install python 3 and pip3 and the following libs + +`pip3 install requests progressbar2` + +move your .couch files into the specified `database_dir` in your [fauxton config](http://127.0.0.1:5984/_utils/#_config/couchdb@localhost) + + +``` +$ ./couchup list # Shows your unmigrated 1.x databases +$ ./couchup replicate -a # Replicates your 1.x DBs to 2.x +$ ./couchup rebuild -a # Optional; starts rebuilding your views +$ ./couchup delete -a # Deletes your 1.x DBs (careful!) +$ ./couchup list # Should show no remaining databases! +``` diff --git a/lib/dolly/connection.rb b/lib/dolly/connection.rb index 6a72e69..13bd234 100644 --- a/lib/dolly/connection.rb +++ b/lib/dolly/connection.rb @@ -72,7 +72,7 @@ def request(method, resource, data = {}) def start_request(req) Net::HTTP.start(req.uri.hostname, req.uri.port) do |http| - req.basic_auth env['username'], env['password'] if env['username'].present? + req.basic_auth env['username'], env['password'] if env['username']&.present? http.request(req) end end diff --git a/lib/dolly/document.rb b/lib/dolly/document.rb index fd4ef60..e26871b 100644 --- a/lib/dolly/document.rb +++ b/lib/dolly/document.rb @@ -1,3 +1,4 @@ +require 'dolly/mango' require 'dolly/query' require 'dolly/connection' require 'dolly/request' @@ -15,11 +16,13 @@ module Dolly class Document + extend Mango extend Query extend Request extend DepracatedDatabase extend Properties extend DocumentCreation + include PropertyManager include Timestamp include DocumentState diff --git a/lib/dolly/exceptions.rb b/lib/dolly/exceptions.rb index 6e200ee..e0ce22b 100644 --- a/lib/dolly/exceptions.rb +++ b/lib/dolly/exceptions.rb @@ -15,6 +15,16 @@ def to_s end end + class InvalidMangoOperatorError < RuntimeError + def initialize msg + @msg = msg + end + + def to_s + "Invalid Mango operator: #{@msg.inspect}" + end + end + class InvalidConfigFileError < RuntimeError; end class InvalidProperty < RuntimeError; end class DocumentInvalidError < RuntimeError; end diff --git a/lib/dolly/mango.rb b/lib/dolly/mango.rb new file mode 100644 index 0000000..85bbb5c --- /dev/null +++ b/lib/dolly/mango.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'refinements/hash_refinements' + +module Dolly + module Mango + using HashRefinements + + SELECTOR_SYMBOL = '$' + + COMBINATION_OPERATORS = %I[ + and + or + not + nor + all + elemMatch + allMath + ].freeze + + CONDITION_OPERATORS = %I[ + lt + lte + eq + ne + gte + gt + exists + type + in + nin + size + mod + regex + ].freeze + + ALL_OPERATORS = COMBINATION_OPERATORS + CONDITION_OPERATORS + + DESIGN = '_find' + + def find_by(query, opts = {}) + build_model_from_doc(find_doc_by(query, opts)) + end + + def find_doc_by(query, opts = {}) + opts.merge!(limit: 1) + perform_query(build_query(query, opts))[:docs].first + end + + def where(query, opts = {}) + docs_where(query, opts).map do |doc| + build_model_from_doc(doc) + end + end + + def docs_where(query, opts = {}) + perform_query(build_query(query, opts))[:docs] + end + + private + + def build_model_from_doc(doc) + return nil if doc.nil? + new(doc.slice(*all_property_keys)) + end + + def perform_query(structured_query) + connection.post(DESIGN, structured_query) + end + + def build_query(query, opts) + { 'selector' => build_selectors(query) }.merge(opts) + end + + def build_selectors(query) + query.deep_transform_keys do |key| + next build_key(key) if is_operator?(key) + raise InvalidMangoOperatorError.new(key) unless self.all_property_keys.include?(key) + key + end + end + + def build_key(key) + "#{SELECTOR_SYMBOL}#{key}" + end + + def is_operator?(key) + ALL_OPERATORS.include?(key) + end + end +end diff --git a/lib/dolly/properties.rb b/lib/dolly/properties.rb index 3af9d28..4a24ff7 100644 --- a/lib/dolly/properties.rb +++ b/lib/dolly/properties.rb @@ -20,8 +20,12 @@ def properties @properties ||= PropertySet.new end + def all_property_keys + properties.map(&:key) + SPECIAL_KEYS + end + def property_keys - properties.map(&:key) - SPECIAL_KEYS + all_property_keys - SPECIAL_KEYS end def property_clean_doc(doc) diff --git a/lib/refinements/hash_refinements.rb b/lib/refinements/hash_refinements.rb new file mode 100644 index 0000000..9afdcf9 --- /dev/null +++ b/lib/refinements/hash_refinements.rb @@ -0,0 +1,27 @@ +module HashRefinements + refine Hash do + # File activesupport/lib/active_support/core_ext/hash/keys.rb, line 82 + def deep_transform_keys(&block) + _deep_transform_keys_in_object(self, &block) + end + + def slice(*keys) + keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) } + end + + private + + def _deep_transform_keys_in_object(object, &block) + case object + when Hash + object.each_with_object({}) do |(key, value), result| + result[yield(key)] = _deep_transform_keys_in_object(value, &block) + end + when Array + object.map { |e| _deep_transform_keys_in_object(e, &block) } + else + object + end + end + end +end diff --git a/test/document_test.rb b/test/document_test.rb index 4ddafcf..09eda44 100644 --- a/test/document_test.rb +++ b/test/document_test.rb @@ -1,7 +1,5 @@ require 'test_helper' -class BaseDolly < Dolly::Document; end - class BarFoo < BaseDolly property :a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m, :n, :persist end diff --git a/test/mango_test.rb b/test/mango_test.rb new file mode 100644 index 0000000..b9c0942 --- /dev/null +++ b/test/mango_test.rb @@ -0,0 +1,164 @@ +require 'test_helper' + +class FooBar < BaseDolly + property :foo, :bar + property :with_default, default: 1 + property :boolean, class_name: TrueClass, default: true + property :date, class_name: Date + property :time, class_name: Time + property :datetime, class_name: DateTime + property :is_nil, class_name: NilClass, default: nil + + timestamps! +end + +class MangoTest < Test::Unit::TestCase + DB_BASE_PATH = "http://localhost:5984/test".freeze + + def setup + data = {foo: 'Foo', bar: 'Bar', type: 'foo_bar'} + + all_docs = [ {foo: 'Foo B', bar: 'Bar B', type: 'foo_bar'}, {foo: 'Foo A', bar: 'Bar A', type: 'foo_bar'}] + + view_resp = build_view_response [data] + empty_resp = build_view_response [] + not_found_resp = generic_response [{ key: "foo_bar/2", error: "not_found" }] + @multi_resp = build_view_response all_docs + @multi_type_resp = build_view_collation_response all_docs + + build_request [["foo_bar","1"]], view_resp + build_request [["foo_bar","2"]], empty_resp + build_request [["foo_bar","1"],["foo_bar","2"]], @multi_resp + + stub_request(:get, "#{query_base_path}?startkey=%22foo_bar%2F%22&endkey=%22foo_bar%2F%EF%BF%B0%22&include_docs=true"). + to_return(body: @multi_resp.to_json) + end + + test '#find_by' do + #TODO: clean up all the fake request creation + resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] } + + stub_request(:post, query_base_path). + to_return(body: resp.to_json) + + assert_equal(FooBar.find_by(foo: 'bar').class, FooBar) + end + + test '#find_by with no returned data' do + resp = { docs: [] } + + stub_request(:post, query_base_path). + to_return(body: resp.to_json) + + assert_equal(FooBar.find_by(foo: 'bar'), nil) + end + + test '#find_doc_by' do + #TODO: clean up all the fake request creation + resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] } + + stub_request(:post, query_base_path). + to_return(body: resp.to_json) + + assert_equal(FooBar.find_doc_by(foo: 'bar').class, Hash) + end + + test '#where' do + #TODO: clean up all the fake request creation + resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] } + + stub_request(:post, query_base_path). + to_return(body: resp.to_json) + + assert_equal(FooBar.where(foo: { eq: 'bar' }).map(&:class).uniq, [FooBar]) + end + + test '#where with no returned data' do + resp = { docs: [] } + + stub_request(:post, query_base_path). + to_return(body: resp.to_json) + + assert_equal(FooBar.where(foo: 'bar'), []) + end + + test '#docs_where' do + #TODO: clean up all the fake request creation + resp = { docs: [{ foo: 'bar', id: "foo_bar/1"} ] } + + stub_request(:post, query_base_path). + to_return(body: resp.to_json) + + assert_equal(FooBar.docs_where(foo: { eq: 'bar' }).map(&:class).uniq, [Hash]) + end + + test '#build_query' do + query = { and: [{ _id: { eq: 'foo_bar/1' } } , { foo: { eq: 'bar'}} ] } + opts = {} + expected = {"selector"=>{"$and"=>[{:_id=>{"$eq"=>"foo_bar/1"}}, {:foo=>{"$eq"=>"bar"}}]}} + + assert_equal(FooBar.send(:build_query, query, opts), expected) + end + + test '#build_query with options' do + query = { and: [{ _id: { eq: 'foo_bar/1' } } , { foo: { eq: 'bar'}} ] } + opts = { limit: 1, fields: ['foo']} + expected = {"selector"=>{"$and"=>[{:_id=>{"$eq"=>"foo_bar/1"}}, {:foo=>{"$eq"=>"bar"}}]}, limit: 1, fields: ['foo']} + + assert_equal(FooBar.send(:build_query, query, opts), expected) + end + + test '#build_selectors with invalid operator' do + query = { _id: { eeeq: 'foo_bar/1' } } + + assert_raise Dolly::InvalidMangoOperatorError do + FooBar.send(:build_selectors, query) + end + end + + private + def generic_response rows, count = 1 + {total_rows: count, offset:0, rows: rows} + end + + def build_view_response properties + rows = properties.map.with_index do |v, i| + { + id: "foo_bar/#{i}", + key: "foo_bar", + value: 1, + doc: {_id: "foo_bar/#{i}", _rev: SecureRandom.hex}.merge!(v) + } + end + generic_response rows, properties.count + end + + def build_view_collation_response properties + rows = properties.map.with_index do |v, i| + id = i.zero? ? "foo_bar/#{i}" : "baz/#{i}" + { + id: id, + key: "foo_bar", + value: 1, + doc: {_id: id, _rev: SecureRandom.hex}.merge!(v) + } + end + generic_response rows, properties.count + end + + + def build_request keys, body, view_name = 'foo_bar' + query = "keys=#{CGI::escape keys.to_s.gsub(' ','')}&" unless keys&.empty? + stub_request(:get, "#{query_base_path}?#{query.to_s}include_docs=true"). + to_return(body: body.to_json) + end + + def query_base_path + "#{DB_BASE_PATH}/_find" + end + + def build_save_request(obj) + stub_request(:put, "#{DB_BASE_PATH}/#{CGI.escape(obj.id)}"). + to_return(body: {ok: true, id: obj.id, rev: "FF0000" }.to_json) + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 69681c7..61766bf 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -29,3 +29,5 @@ def base_path %r{http://.*:5984/#{DEFAULT_DB}} end end + +class BaseDolly < Dolly::Document; end