diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd8c3834..bbaff0ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,30 +10,36 @@ jobs: test: strategy: matrix: - gemfile: [ '7.0.0', '5.1.7' ] - ruby: [ '3.0', '2.7', '2.6'] - exclude: + include: - ruby: '3.0' - gemfile: '5.1.7' + gemfile: '7.0.0' + couchbase: '6.6.5' + - ruby: '3.0' + gemfile: '7.0.0' + couchbase: '7.1.0' - ruby: '2.7' - gemfile: '5.1.7' - - ruby: '2.6' gemfile: '7.0.0' + couchbase: '7.1.0' + - ruby: '2.6' + gemfile: '5.1.7' + couchbase: '7.1.0' fail-fast: false - runs-on: ubuntu-18.04 - name: ${{ matrix.ruby }} ${{ matrix.database }} rails-${{ matrix.gemfile }} + runs-on: ubuntu-20.04 + name: ${{ matrix.ruby }} rails-${{ matrix.gemfile }} couchbase-${{ matrix.couchbase }} steps: - uses: actions/checkout@v2 - - run: sudo apt-get update && sudo apt-get install libevent-dev + - run: sudo apt-get update && sudo apt-get install libevent-dev libev-dev python-httplib2 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - - run: sudo ./ci/run_couchbase.sh + - run: sudo ./ci/run_couchbase.sh $COUCHBASE_VERSION $COUCHBASE_BUCKET $COUCHBASE_USER $COUCHBASE_PASSWORD - run: bundle exec rspec env: ACTIVE_MODEL_VERSION: ${{ matrix.gemfile }} BUNDLE_JOBS: 4 BUNDLE_PATH: vendor/bundle - TRAVIS_TEST: true - RAILS_ENV: test + COUCHBASE_BUCKET: default + COUCHBASE_USER: tester + COUCHBASE_PASSWORD: password123 + COUCHBASE_VERSION: ${{ matrix.couchbase }} diff --git a/README.md b/README.md index 8f471af7..2d40914d 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ To generate config you can use `rails generate couchbase_orm:config`: It will generate this `config/couchbase.yml` for you: +```yaml common: &common - hosts: localhost + connection_string: couchbase://localhost username: dev_user password: dev_password @@ -26,10 +27,23 @@ It will generate this `config/couchbase.yml` for you: # set these environment variables on your production server production: - hosts: <%= ENV['COUCHBASE_HOST'] || ENV['COUCHBASE_HOSTS'] %> - bucket: <%= ENV['COUCHBASE_BUCKET'] %> - username: <%= ENV['COUCHBASE_USER'] %> - password: <%= ENV['COUCHBASE_PASSWORD'] %> + connection_string: <%= ENV['COUCHBASE_CONNECTION_STRING'] %> + bucket: <%= ENV['COUCHBASE_BUCKET'] %> + username: <%= ENV['COUCHBASE_USER'] %> + password: <%= ENV['COUCHBASE_PASSWORD'] %> +``` + +## Setup without Rails +If you are not using Rails, you can configure couchbase-orm with an initializer: +```ruby +# config/initializers/couchbase_orm.rb +CouchbaseOrm::Connection.config = { + connection_string: "couchbase://localhost" + username: "dev_user" + password: "dev_password" + bucket: "dev_bucket" +} +``` Views are generated on application load if they don't exist or mismatch. This works fine in production however by default in development models are lazy loaded. diff --git a/ci/run_couchbase.sh b/ci/run_couchbase.sh index 8100453a..912938bf 100755 --- a/ci/run_couchbase.sh +++ b/ci/run_couchbase.sh @@ -1,14 +1,20 @@ set -x set -e -apt-get install libev-dev python-httplib2 libssl1.0.0 -wget https://packages.couchbase.com/releases/5.1.0/couchbase-server-enterprise_5.1.0-ubuntu14.04_amd64.deb -dpkg -i couchbase-server-enterprise_5.1.0-ubuntu14.04_amd64.deb + +VERSION=$1 +BUCKET=$2 +USER=$3 +PASSWORD=$4 + + +wget https://packages.couchbase.com/releases/$VERSION/couchbase-server-enterprise_$VERSION-ubuntu20.04_amd64.deb +dpkg -i couchbase-server-enterprise_$VERSION-ubuntu20.04_amd64.deb sleep 8 sudo service couchbase-server status /opt/couchbase/bin/couchbase-cli cluster-init -c 127.0.0.1:8091 --cluster-username=admin --cluster-password=password --cluster-ramsize=320 --cluster-index-ramsize=256 --cluster-fts-ramsize=256 --services=data,index,query,fts sleep 5 /opt/couchbase/bin/couchbase-cli server-info -c 127.0.0.1:8091 -u admin -p password -/opt/couchbase/bin/couchbase-cli bucket-create -c 127.0.0.1:8091 -u admin -p password --bucket=default --bucket-type=couchbase --bucket-ramsize=160 --bucket-replica=0 --wait +/opt/couchbase/bin/couchbase-cli bucket-create -c 127.0.0.1:8091 -u admin -p password --bucket=$BUCKET --bucket-type=couchbase --bucket-ramsize=160 --bucket-replica=0 --wait sleep 1 -/opt/couchbase/bin/couchbase-cli user-manage -c 127.0.0.1:8091 -u admin -p password --set --rbac-username tester --rbac-password password123 --rbac-name "Auto Tester" --roles admin --auth-domain local -curl http://admin:password@localhost:8093/query/service -d 'statement=CREATE INDEX `default_type` ON `default`(`type`)' +/opt/couchbase/bin/couchbase-cli user-manage -c 127.0.0.1:8091 -u admin -p password --set --rbac-username $USER --rbac-password $PASSWORD --rbac-name "Auto Tester" --roles admin --auth-domain local +curl http://admin:password@localhost:8093/query/service -d "statement=CREATE INDEX \`default_type\` ON \`$BUCKET\`(\`type\`)" diff --git a/couchbase-orm.gemspec b/couchbase-orm.gemspec index a3d23ce1..79eba3a5 100644 --- a/couchbase-orm.gemspec +++ b/couchbase-orm.gemspec @@ -13,14 +13,15 @@ Gem::Specification.new do |gem| gem.required_ruby_version = '>= 2.1.0' gem.require_paths = ["lib"] - gem.add_runtime_dependency 'mt-libcouchbase', '~> 1.2' gem.add_runtime_dependency 'activemodel', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.0' + gem.add_runtime_dependency 'couchbase' gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base gem.add_development_dependency 'rake', '~> 12.2' gem.add_development_dependency 'rspec', '~> 3.7' gem.add_development_dependency 'yard', '~> 0.9' - gem.add_development_dependency 'minitest', '~> 5.10' + gem.add_development_dependency 'pry' + gem.add_development_dependency 'simplecov' gem.files = `git ls-files`.split("\n") gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index 71a1d1af..4c6257a6 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true, encoding: ASCII-8BIT -require 'mt-libcouchbase' -MTLibcouchbase.autoload(:QueryN1QL, 'ext/query_n1ql') - module CouchbaseOrm autoload :Error, 'couchbase-orm/error' autoload :Connection, 'couchbase-orm/connection' @@ -10,37 +7,45 @@ module CouchbaseOrm autoload :Base, 'couchbase-orm/base' autoload :HasMany, 'couchbase-orm/utilities/has_many' + def self.logger + @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT) + end + + def self.logger=(logger) + @@logger = logger + end + def self.try_load(id) result = nil was_array = id.is_a?(Array) if was_array && id.length == 1 - id = id.first - end - result = id.respond_to?(:cas) ? id : CouchbaseOrm::Base.bucket.get(id, quiet: true, extended: true) - if was_array - result = Array.wrap(result) - end - if result && result.is_a?(Array) - return result.map { |r| self.try_load(r) }.compact + query_id = id.first + else + query_id = id end - if result && result.value.is_a?(Hash) && result.value[:type] - ddoc = result.value[:type] - ::CouchbaseOrm::Base.descendants.each do |model| - if model.design_document == ddoc - return model.new(result) - end - end + result = query_id.is_a?(Array) ? CouchbaseOrm::Base.bucket.default_collection.get_multi(query_id) : CouchbaseOrm::Base.bucket.default_collection.get(query_id) + + result = Array.wrap(result) if was_array + + if result&.is_a?(Array) + return result.zip(id).map { |r, id| try_load_create_model(r, id) }.compact end - nil - end - def self.logger - @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT) + return try_load_create_model(result, id) end - def self.logger=(logger) - @@logger = logger + private + + def self.try_load_create_model(result, id) + ddoc = result&.content["type"] + return nil unless ddoc + ::CouchbaseOrm::Base.descendants.each do |model| + if model.design_document == ddoc + return model.new(result, id: id) + end + end + nil end end diff --git a/lib/couchbase-orm/associations.rb b/lib/couchbase-orm/associations.rb index c0507f20..0d21f94c 100644 --- a/lib/couchbase-orm/associations.rb +++ b/lib/couchbase-orm/associations.rb @@ -75,12 +75,15 @@ def has_and_belongs_to_many(name, **options) # Define reader define_method(name) do return instance_variable_get(instance_var) if instance_variable_defined?(instance_var) + ref_value = self.send(ref) + ref_value = nil if ref_value.respond_to?(:empty?) && ref_value.empty? + val = if options[:polymorphic] - ::CouchbaseOrm.try_load(self.send(ref)) + ::CouchbaseOrm.try_load(ref_value) if ref_value else - assoc.constantize.find(self.send(ref), quiet: true) + assoc.constantize.find(ref_value) if ref_value end - val = Array.wrap(val) + val = Array.wrap(val || []) instance_variable_set(instance_var, val) val end @@ -181,7 +184,7 @@ def destroy_associations! when :destroy, :delete if model.respond_to?(:stream) model.stream { |mod| mod.__send__(dependent) } - elsif model.is_a?(Array) + elsif model.is_a?(Array) || model.is_a?(CouchbaseOrm::ResultsProxy) model.each { |m| m.__send__(dependent) } else model.__send__(dependent) diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 4eebbdef..185d7ad6 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -3,12 +3,14 @@ require 'active_model' require 'active_support/hash_with_indifferent_access' +require 'couchbase' require 'couchbase-orm/error' require 'couchbase-orm/views' require 'couchbase-orm/n1ql' require 'couchbase-orm/persistence' require 'couchbase-orm/associations' require 'couchbase-orm/proxies/bucket_proxy' +require 'couchbase-orm/proxies/collection_proxy' require 'couchbase-orm/utilities/join' require 'couchbase-orm/utilities/enum' require 'couchbase-orm/utilities/index' @@ -55,6 +57,14 @@ def bucket @bucket ||= BucketProxy.new(Connection.bucket) end + def cluster + Connection.cluster + end + + def collection + CollectionProxy.new(bucket.default_collection) + end + def uuid_generator @uuid_generator ||= IdGenerator end @@ -90,27 +100,20 @@ def attributes @attributes ||= {} end - def find(*ids, **options) - options[:extended] = true - options[:quiet] ||= false + def find(*ids, quiet: false) + CouchbaseOrm.logger.debug { "Base.find(l##{ids.length}) #{ids}" } ids = ids.flatten.select { |id| id.present? } if ids.empty? - return nil if options[:quiet] - raise MTLibcouchbase::Error::EmptyKey, 'no id(s) provided' + raise CouchbaseOrm::Error::EmptyNotAllowed, 'no id(s) provided' end - CouchbaseOrm.logger.debug "Data - Get #{ids}" - record = bucket.get(*ids, **options) - records = record.is_a?(Array) ? record : [record] - records.map! { |record| - if record - self.new(record) - else - false - end + records = quiet ? collection.get_multi(ids) : collection.get_multi!(ids) + CouchbaseOrm.logger.debug { "Base.find found(#{records})" } + records = records.zip(ids).map { |record, id| + self.new(record, id: id) if record } - records.select! { |rec| rec } + records.compact! ids.length > 1 ? records : records[0] end @@ -121,12 +124,13 @@ def find_by_id(*ids, **options) alias_method :[], :find_by_id def exists?(id) - CouchbaseOrm.logger.debug "Data - Get #{id}" - !bucket.get(id, quiet: true).nil? + CouchbaseOrm.logger.debug "Data - Exists? #{id}" + collection.exists(id).exists end alias_method :has_key?, :exists? end + class MismatchTypeError < RuntimeError; end # Add support for libcouchbase response objects def initialize(model = nil, ignore_doc_type: false, **attributes) @@ -145,25 +149,28 @@ def initialize(model = nil, ignore_doc_type: false, **attributes) if model case model - when ::MTLibcouchbase::Response - doc = model.value || raise('empty response provided') - type = doc.delete(:type) + when Couchbase::Collection::GetResult + CouchbaseOrm.logger.debug "Initialize with Couchbase::Collection::GetResult" + doc = model.content || raise('empty response provided') + type = doc.delete('type') doc.delete(:id) if type && !ignore_doc_type && type.to_s != self.class.design_document - raise "document type mismatch, #{type} != #{self.class.design_document}" + raise CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self) end - @__metadata__.key = model.key + @__metadata__.key = attributes[:id] @__metadata__.cas = model.cas # This ensures that defaults are applied @__attributes__.merge! doc - clear_changes_information when CouchbaseOrm::Base + CouchbaseOrm.logger.debug "Initialize with CouchbaseOrm::Base" + clear_changes_information attributes = model.attributes attributes.delete(:id) + attributes.delete('type') super(attributes) else clear_changes_information diff --git a/lib/couchbase-orm/connection.rb b/lib/couchbase-orm/connection.rb index 33235649..cda9dc76 100644 --- a/lib/couchbase-orm/connection.rb +++ b/lib/couchbase-orm/connection.rb @@ -1,16 +1,36 @@ -# frozen_string_literal: true, encoding: ASCII-8BIT - -require 'mt-libcouchbase' +require 'couchbase' module CouchbaseOrm class Connection - @options = {} - class << self - attr_accessor :options + @@config = nil + def self.config + @@config || { + :connection_string => "couchbase://#{ENV['COUCHBASE_HOST'] || '127.0.0.1'}", + :username => ENV['COUCHBASE_USER'], + :password => ENV['COUCHBASE_PASSWORD'], + :bucket => ENV['COUCHBASE_BUCKET'] + } + end + + def self.config=(config) + @@config = config + end + + def self.cluster + @cluster ||= begin + cb_config = Couchbase::Configuration.new + cb_config.connection_string = config[:connection_string] || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm connection string') + cb_config.username = config[:username] || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm username') + cb_config.password = config[:password] || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm password') + Couchbase::Cluster.connect(cb_config) + end end def self.bucket - @bucket ||= ::MTLibcouchbase::Bucket.new(**@options) + @bucket ||= begin + bucket_name = config[:bucket] || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm bucket name') + cluster.bucket(bucket_name) + end end end -end +end \ No newline at end of file diff --git a/lib/couchbase-orm/error.rb b/lib/couchbase-orm/error.rb index 8add65c3..6aa7ebcc 100644 --- a/lib/couchbase-orm/error.rb +++ b/lib/couchbase-orm/error.rb @@ -22,6 +22,8 @@ def initialize(message = nil, record = nil) super(message, record) end end + class TypeMismatchError < Error; end class RecordExists < Error; end + class CouchbaseOrm::Error::EmptyNotAllowed < Error; end end end diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index de30d5ef..2f4562f7 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -31,11 +31,11 @@ module ClassMethods # n1ql :by_rating, emit_key: :rating # end # - # Post.by_rating.stream do |response| + # Post.by_rating do |response| # # ... # end # TODO: add range keys [:startkey, :endkey] - def n1ql(name, query: nil, emit_key: [], **options) + def n1ql(name, query_fn: nil, emit_key: [], **options) emit_key = Array.wrap(emit_key) emit_key.each do |key| raise "unknown emit_key attribute for n1ql :#{name}, emit_key: :#{key}" if key && @attributes[key].nil? @@ -48,10 +48,9 @@ def n1ql(name, query: nil, emit_key: [], **options) @indexes[name] = method_opts singleton_class.__send__(:define_method, name) do |**opts, &result_modifier| - opts = options.merge(opts) - - values = convert_values(opts[:key]) - current_query = build_query(method_opts[:emit_key], values, query, **opts) + opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus) + values = convert_values(opts.delete(:key)) + current_query = run_query(method_opts[:emit_key], values, query_fn, **opts.except(:include_docs)) if result_modifier opts[:include_docs] = true @@ -110,24 +109,22 @@ def build_order(keys, descending) "#{keys.dup.push("meta().id").map { |k| "#{k} #{descending ? "desc" : "asc" }" }.join(",")}" end - def build_query(keys, values, query, descending: false, limit: nil, **options) - if query - query.call(bucket, values) + def build_limit(limit) + limit ? "limit #{limit}" : "" + end + + def run_query(keys, values, query_fn, descending: false, limit: nil, **options) + if query_fn + N1qlProxy.new(query_fn.call(bucket, values, Couchbase::Options::Query.new(**options))) else - bucket_name = bucket.bucket + bucket_name = bucket.name where = build_where(keys, values) order = build_order(keys, descending) - query = bucket.n1ql - .select("raw meta().id") - .from("`#{bucket_name}`") - .where(where) - if order - query = query.order_by(order) - end - if limit - query = query.limit(limit) - end - query + limit = build_limit(limit) + n1ql_query = "select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}" + result = cluster.query(n1ql_query, Couchbase::Options::Query.new(**options)) + CouchbaseOrm.logger.debug "N1QL query: #{n1ql_query} return #{result.rows.to_a.length} rows" + N1qlProxy.new(result) end end end diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 194bb4f6..6e30e820 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -100,7 +100,7 @@ def save!(**options) def delete(with_cas: false, **options) options[:cas] = @__metadata__.cas if with_cas CouchbaseOrm.logger.debug "Data - Delete #{@__metadata__.key}" - self.class.bucket.delete(@__metadata__.key, **options) + self.class.collection.remove(@__metadata__.key, **options) @__metadata__.key = nil @id = nil @@ -110,6 +110,8 @@ def delete(with_cas: false, **options) self end + alias :remove :delete + # Deletes the record in the database and freezes this instance to reflect # that no changes should be made (since they can't be persisted). # @@ -123,7 +125,7 @@ def destroy(with_cas: false, **options) options[:cas] = @__metadata__.cas if with_cas CouchbaseOrm.logger.debug "Data - Delete #{@__metadata__.key}" - self.class.bucket.delete(@__metadata__.key, **options) + self.class.collection.remove(@__metadata__.key, **options) @__metadata__.key = nil @id = nil @@ -160,7 +162,10 @@ def update!(hash) end alias_method :update_attributes!, :update! - # Updates the record without validating or running callbacks + # Updates the record without validating or running callbacks. + # Updates only the attributes that are passed in as parameters + # except if there is more than 16 attributes, in which case + # the whole record is saved. def update_columns(with_cas: false, **hash) _id = @__metadata__.key raise "unable to update columns, model not persisted" unless _id @@ -172,21 +177,19 @@ def update_columns(with_cas: false, **hash) # There is a limit of 16 subdoc operations per request resp = if hash.length <= 16 - subdoc = self.class.bucket.subdoc(_id) - hash.each do |key, value| - subdoc.dict_upsert(key, value) - end - subdoc.execute!(**options) + self.class.collection.mutate_in( + _id, + hash.map { |k, v| Couchbase::MutateInSpec.replace(k.to_s, v) } + ) else # Fallback to writing the whole document @__attributes__[:type] = self.class.design_document @__attributes__.delete(:id) CouchbaseOrm.logger.debug { "Data - Replace #{_id} #{@__attributes__.to_s.truncate(200)}" } - self.class.bucket.replace(_id, @__attributes__, **options) + self.class.collection.replace(_id, @__attributes__, **options) end # Ensure the model is up to date - @__metadata__.key = resp.key @__metadata__.cas = resp.cas changes_applied @@ -201,9 +204,9 @@ def reload raise "unable to reload, model not persisted" unless key CouchbaseOrm.logger.debug "Data - Get #{key}" - resp = self.class.bucket.get(key, quiet: false, extended: true) - @__attributes__ = ::ActiveSupport::HashWithIndifferentAccess.new(resp.value) - @__metadata__.key = resp.key + resp = self.class.collection.get!(key) + @__attributes__ = ::ActiveSupport::HashWithIndifferentAccess.new(resp.content) + @__metadata__.key = key @__metadata__.cas = resp.cas reset_associations @@ -214,7 +217,7 @@ def reload # Updates the TTL of the document def touch(**options) CouchbaseOrm.logger.debug "Data - Touch #{@__metadata__.key}" - res = self.class.bucket.touch(@__metadata__.key, async: false, **options) + res = self.class.collection.touch(@__metadata__.key, async: false, **options) @__metadata__.cas = resp.cas self end @@ -235,11 +238,11 @@ def _update_record(with_cas: false, **options) _id = @__metadata__.key options[:cas] = @__metadata__.cas if with_cas - CouchbaseOrm.logger.debug { "Data - Replace #{_id} #{@__attributes__.to_s.truncate(200)}" } - resp = self.class.bucket.replace(_id, @__attributes__, **options) + CouchbaseOrm.logger.debug { "_update_record - replace #{_id} #{@__attributes__.to_s.truncate(200)}" } + resp = self.class.collection.replace(_id, @__attributes__, Couchbase::Options::Replace.new(**options)) # Ensure the model is up to date - @__metadata__.key = resp.key + @__metadata__.key = _id @__metadata__.cas = resp.cas changes_applied @@ -247,7 +250,6 @@ def _update_record(with_cas: false, **options) end end end - def _create_record(**options) return false unless perform_validations(:create, options) @@ -258,11 +260,13 @@ def _create_record(**options) @__attributes__.delete(:id) _id = @id || self.class.uuid_generator.next(self) - CouchbaseOrm.logger.debug { "Data - Insert #{_id} #{@__attributes__.to_s.truncate(200)}" } - resp = self.class.bucket.add(_id, @__attributes__, **options) + CouchbaseOrm.logger.debug { "_create_record - Upsert #{_id} #{@__attributes__.to_s.truncate(200)}" } + #resp = self.class.collection.add(_id, @__attributes__, **options) + + resp = self.class.collection.upsert(_id, @__attributes__, Couchbase::Options::Upsert.new(**options)) # Ensure the model is up to date - @__metadata__.key = resp.key + @__metadata__.key = _id @__metadata__.cas = resp.cas changes_applied diff --git a/lib/couchbase-orm/proxies/bucket_proxy.rb b/lib/couchbase-orm/proxies/bucket_proxy.rb index 810a5c80..259ecd1e 100644 --- a/lib/couchbase-orm/proxies/bucket_proxy.rb +++ b/lib/couchbase-orm/proxies/bucket_proxy.rb @@ -5,11 +5,9 @@ module CouchbaseOrm class BucketProxy def initialize(proxyfied) - @proxyfied = proxyfied + raise ArgumentError, "Must proxy a non nil object" if proxyfied.nil? - self.class.define_method(:name) do - @proxyfied.bucket - end + @proxyfied = proxyfied self.class.define_method(:n1ql) do N1qlProxy.new(@proxyfied.n1ql) @@ -23,18 +21,15 @@ def initialize(proxyfied) CouchbaseOrm.logger.debug "View - #{design} #{view}" @results = ResultsProxy.new(@proxyfied.send(:view, design, view, **opts, &block)) end - - proxyfied.public_methods.each do |method| - next if self.public_methods.include?(method) - if RUBY_VERSION.to_i >= 3 - self.class.define_method(method) do |*params, **options, &block| - @proxyfied.send(method, *params, **options, &block) - end - else - self.class.define_method(method) do |*params, &block| - @proxyfied.send(method, *params, &block) - end - end + end + + if RUBY_VERSION.to_i >= 3 + def method_missing(name, *args, **options, &block) + @proxyfied.public_send(name, *args, **options, &block) + end + else + def method_missing(name, *args, &block) + @proxyfied.public_send(name, *args, &block) end end end diff --git a/lib/couchbase-orm/proxies/collection_proxy.rb b/lib/couchbase-orm/proxies/collection_proxy.rb new file mode 100644 index 00000000..9ddf54c6 --- /dev/null +++ b/lib/couchbase-orm/proxies/collection_proxy.rb @@ -0,0 +1,53 @@ +require "couchbase" + +module CouchbaseOrm + class CollectionProxy + + def get!(id, **options) + @proxyfied.get(id, Couchbase::Options::Get.new(**options)) + end + + def get(id, **options) + @proxyfied.get(id, Couchbase::Options::Get.new(**options)) + rescue Couchbase::Error::DocumentNotFound + nil + end + + def get_multi!(*ids, **options) + result = @proxyfied.get_multi(*ids, Couchbase::Options::GetMulti.new(**options)) + first_result_with_error = result.find(&:error) + raise first_result_with_error.error if first_result_with_error + result + end + + def get_multi(*ids, **options) + result = @proxyfied.get_multi(*ids, Couchbase::Options::GetMulti.new(**options)) + result.reject(&:error) + end + + def remove!(id, **options) + @proxyfied.remove(id, Couchbase::Options::Remove.new(**options)) + end + + def remove(id, **options) + @proxyfied.remove(id, Couchbase::Options::Remove.new(**options)) + rescue Couchbase::Error::DocumentNotFound + nil + end + + def initialize(proxyfied) + raise "Must proxy a non nil object" if proxyfied.nil? + @proxyfied = proxyfied + end + + if RUBY_VERSION.to_i >= 3 + def method_missing(name, *args, **options, &block) + @proxyfied.public_send(name, *args, **options, &block) + end + else # :nocov: + def method_missing(name, *args, &block) + @proxyfied.public_send(name, *args, &block) + end + end + end +end diff --git a/lib/couchbase-orm/proxies/n1ql_proxy.rb b/lib/couchbase-orm/proxies/n1ql_proxy.rb index 688e29ba..62a8ab3b 100644 --- a/lib/couchbase-orm/proxies/n1ql_proxy.rb +++ b/lib/couchbase-orm/proxies/n1ql_proxy.rb @@ -13,7 +13,10 @@ def initialize(proxyfied) return @results if @results CouchbaseOrm.logger.debug 'Query - ' + self.to_s - @results = ResultsProxy.new(@proxyfied.results(*params, &block)) + + results = @proxyfied.rows + results = results.map { |r| block.call(r) } if block + @results = ResultsProxy.new(results.to_a) end self.class.define_method(:to_s) do diff --git a/lib/couchbase-orm/railtie.rb b/lib/couchbase-orm/railtie.rb index 7f5861a4..391512e9 100644 --- a/lib/couchbase-orm/railtie.rb +++ b/lib/couchbase-orm/railtie.rb @@ -24,8 +24,8 @@ module Rails #:nodoc: module Couchbase #:nodoc: class Railtie < Rails::Railtie #:nodoc: - config.couchbase = ActiveSupport::OrderedOptions.new - config.couchbase.ensure_design_documents = true + config.couchbase_orm = ActiveSupport::OrderedOptions.new + config.couchbase_orm.ensure_design_documents = true # Maping of rescued exceptions to HTTP responses # @@ -35,10 +35,6 @@ class Railtie < Rails::Railtie #:nodoc: # @return [Hash] rescued responses def self.rescue_responses { - 'MTLibcouchbase::Error::KeyNotFound' => :not_found, - 'MTLibcouchbase::Error::NotStored' => :unprocessable_entity, - MTLibcouchbase::Error::KeyNotFound => :not_found, - MTLibcouchbase::Error::NotStored => :unprocessable_entity } end @@ -48,14 +44,8 @@ def self.rescue_responses config.action_dispatch.rescue_responses.merge!(rescue_responses) end - # Initialize Couchbase Mode. This will look for a couchbase.yml in the - # config directory and configure Couchbase connection appropriately. - initializer 'couchbase.setup_connection' do - config_file = Rails.root.join('config', 'couchbase.yml') - if config_file.file? && - config = YAML.load(ERB.new(File.read(config_file)).result)[Rails.env] - ::CouchbaseOrm::Connection.options = config.deep_symbolize_keys - end + initializer 'couchbase_orm.setup_connection_config' do + CouchbaseOrm::Connection.config = Rails.application.config_for(:couchbase) end # After initialization we will warn the user if we can't find a couchbase.yml and @@ -83,7 +73,7 @@ def self.rescue_responses # Check (and upgrade if needed) all design documents config.after_initialize do |app| - if config.couchbase.ensure_design_documents + if config.couchbase_orm.ensure_design_documents begin ::CouchbaseOrm::Base.descendants.each do |model| model.ensure_design_document! diff --git a/lib/couchbase-orm/utilities/has_many.rb b/lib/couchbase-orm/utilities/has_many.rb index 632c1ef7..d03458b0 100644 --- a/lib/couchbase-orm/utilities/has_many.rb +++ b/lib/couchbase-orm/utilities/has_many.rb @@ -17,8 +17,8 @@ def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_cla klass = begin class_name.constantize - rescue NameError => e - puts "WARNING: #{class_name} referenced in #{self.name} before it was aded" + rescue NameError + warn "WARNING: #{class_name} referenced in #{self.name} before it was aded" # Open the class early - load order will have to be changed to prevent this. # Warning notice required as a misspelling will not raise an error @@ -42,7 +42,7 @@ class #{class_name} < CouchbaseOrm::Base when :n1ql remote_klass.find(row) when :view - remote_klass.find(row.value[through_key]) + remote_klass.find(row[through_key]) else raise 'type is unknown' end @@ -93,12 +93,9 @@ def build_index_view(klass, remote_class, remote_method, through_key, foreign_ke def build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key) if remote_class klass.class_eval do - n1ql remote_method, query: proc { |bucket, values| - bucket_name = bucket.bucket - bucket.n1ql.select("raw #{through_key}") - .from("`#{bucket_name}`") - .where("type=\"#{design_document}\" and #{foreign_key} = #{values[0]}") - } + n1ql remote_method, query_fn: proc { |bucket, values, options| + cluster.query("SELECT raw #{through_key} FROM `#{bucket.name}` where type = \"#{design_document}\" and #{foreign_key} = #{values[0]}", options) + } end else klass.class_eval do diff --git a/lib/couchbase-orm/utilities/index.rb b/lib/couchbase-orm/utilities/index.rb index ec3b5afe..f12d0ecd 100644 --- a/lib/couchbase-orm/utilities/index.rb +++ b/lib/couchbase-orm/utilities/index.rb @@ -50,13 +50,13 @@ def index(attrs, name = nil, presence: true, &processor) # use the bucket key as an index - lookup records by attr values define_singleton_method(find_by_method) do |*values| key = self.send(class_bucket_key_method, *values) - id = self.bucket.get(key, quiet: true) + id = self.collection.get(key)&.content if id mod = self.find_by_id(id) return mod if mod # Clean up record if the id doesn't exist - self.bucket.delete(key, quiet: true) + self.collection.remove(key) end nil @@ -100,18 +100,17 @@ def index(attrs, name = nil, presence: true, &processor) original_key = instance_variable_get(original_bucket_key_var) if original_key - check_ref_id = record.class.bucket.get(original_key, extended: true, quiet: true) - if check_ref_id && check_ref_id.value == record.id - begin - record.class.bucket.delete(original_key, cas: check_ref_id.cas) - rescue ::MTLibcouchbase::Error::KeyExists - # Errors here can be ignored. Just means the key was updated elswhere + begin + check_ref_id = record.class.collection.get(original_key) + if check_ref_id && check_ref_id.content == record.id + CouchbaseOrm.logger.debug "Removing old key #{original_key}" + record.class.collection.remove(original_key, cas: check_ref_id.cas) end end end unless presence == false && attrs.length == 1 && record[attrs[0]].nil? - record.class.bucket.set(record.send(bucket_key_method), record.id, plain: true) + record.class.collection.upsert(record.send(bucket_key_method), record.id) end instance_variable_set(original_bucket_key_var, nil) end @@ -119,13 +118,9 @@ def index(attrs, name = nil, presence: true, &processor) # cleanup by removing the bucket key before the record is deleted # TODO: handle unpersisted, modified component values before_destroy do |record| - check_ref_id = record.class.bucket.get(record.send(bucket_key_method), extended: true, quiet: true) - if check_ref_id && check_ref_id.value == record.id - begin - record.class.bucket.delete(record.send(bucket_key_method), cas: check_ref_id.cas) - rescue ::MTLibcouchbase::Error::KeyExists - # Errors here can be ignored. Just means the key was updated elswhere - end + check_ref_id = record.class.collection.get(record.send(bucket_key_method)) + if check_ref_id && check_ref_id.content == record.id + record.class.collection.remove(record.send(bucket_key_method), cas: check_ref_id.cas) end true end diff --git a/lib/couchbase-orm/views.rb b/lib/couchbase-orm/views.rb index ad8ef33e..530554cf 100644 --- a/lib/couchbase-orm/views.rb +++ b/lib/couchbase-orm/views.rb @@ -19,7 +19,7 @@ module ClassMethods # view :by_rating, emit_key: :rating # end # - # Post.by_rating.stream do |response| + # Post.by_rating do |response| # # ... # end def view(name, map: nil, emit_key: nil, reduce: nil, **options) @@ -78,17 +78,14 @@ def view(name, map: nil, emit_key: nil, reduce: nil, **options) @views[name] = method_opts singleton_class.__send__(:define_method, name) do |**opts, &result_modifier| - opts = options.merge(opts) - + opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus) + CouchbaseOrm.logger.debug("View [#{@design_document}, #{name.inspect}] options: #{opts.inspect}") if result_modifier - opts[:include_docs] = true - bucket.view(@design_document, name, **opts, &result_modifier) + include_docs(bucket.view_query(@design_document, name.to_s, Couchbase::Options::View.new(**opts.except(:include_docs)))).map(&result_modifier) elsif opts[:include_docs] - bucket.view(@design_document, name, **opts) { |row| - self.new(row) - } + include_docs(bucket.view_query(@design_document, name.to_s, Couchbase::Options::View.new(**opts.except(:include_docs)))) else - bucket.view(@design_document, name, **opts) + bucket.view_query(@design_document, name.to_s, Couchbase::Options::View.new(**opts.except(:include_docs))) end end end @@ -116,26 +113,28 @@ def ensure_design_document! update_required = false # Grab the existing view details - ddoc = bucket.design_docs[@design_document] - existing = ddoc.view_config if ddoc - + begin + ddoc = bucket.view_indexes.get_design_document(@design_document, :production) + rescue Couchbase::Error::DesignDocumentNotFound + end + existing = ddoc.views if ddoc views_actual = {} # Fill in the design documents @views.each do |name, document| - doc = document.dup - views_actual[name] = doc - doc[:map] = doc[:map].gsub('{{design_document}}', @design_document) if doc[:map] - doc[:reduce] = doc[:reduce].gsub('{{design_document}}', @design_document) if doc[:reduce] + views_actual[name.to_s] = Couchbase::Management::View.new( + document[:map]&.gsub('{{design_document}}', @design_document), + document[:reduce]&.gsub('{{design_document}}', @design_document) + ) end # Check there are no changes we need to apply views_actual.each do |name, desired| check = existing[name] if check - cmap = (check[:map] || '').gsub(/\s+/, '') - creduce = (check[:reduce] || '').gsub(/\s+/, '') - dmap = (desired[:map] || '').gsub(/\s+/, '') - dreduce = (desired[:reduce] || '').gsub(/\s+/, '') + cmap = (check.map || '').gsub(/\s+/, '') + creduce = (check.reduce || '').gsub(/\s+/, '') + dmap = (desired.map || '').gsub(/\s+/, '') + dreduce = (desired.reduce || '').gsub(/\s+/, '') unless cmap == dmap && creduce == dreduce update_required = true @@ -149,16 +148,26 @@ def ensure_design_document! # Updated the design document if update_required - bucket.save_design_doc({ - views: views_actual - }, @design_document) + document = Couchbase::Management::DesignDocument.new + document.views = views_actual + document.name = @design_document + bucket.view_indexes.upsert_design_document(document, :production) - puts "Couchbase views updated for #{self.name}, design doc: #{@design_document}" true else false end end + + def include_docs(view_result) + if view_result.rows.length > 1 + self.find(view_result.rows.map(&:id)) + elsif view_result.rows.length == 1 + [self.find(view_result.rows.first.id)] + else + [] + end + end end end end diff --git a/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml b/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml index fefc7479..52821e1c 100644 --- a/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml +++ b/lib/rails/generators/couchbase_orm/config/templates/couchbase.yml @@ -1,5 +1,6 @@ common: &common - hosts: localhost + connection_string: couchbase://localhost + bucket: <%= bucket_name || app_name %> username: <%= username || bucket_name || app_name %> password: <%= password %> @@ -13,7 +14,7 @@ test: # set these environment variables on your production server production: - hosts: <%%= ENV['COUCHBASE_HOST'] || ENV['COUCHBASE_HOSTS'] %> + connection_string: <%%= ENV['COUCHBASE_CONNECTION_STRING'] %> bucket: <%%= ENV['COUCHBASE_BUCKET'] %> username: <%%= ENV['COUCHBASE_USER'] %> password: <%%= ENV['COUCHBASE_PASSWORD'] %> diff --git a/spec/associations_spec.rb b/spec/associations_spec.rb index 850d99b8..ff45ed1d 100644 --- a/spec/associations_spec.rb +++ b/spec/associations_spec.rb @@ -39,21 +39,22 @@ class Part < CouchbaseOrm::Base expect(parent.persisted?).to be(true) expect(child.persisted?).to be(true) id = parent.id - + child.destroy expect(child.destroyed?).to be(true) expect(parent.destroyed?).to be(false) # Ensure that parent has been destroyed - expect { Parent.find(id) }.to raise_error(MTLibcouchbase::Error::KeyNotFound) + expect { Parent.find(id) }.to raise_error(Couchbase::Error::DocumentNotFound) + expect(Parent.find_by_id(id)).to be(nil) - expect { parent.reload }.to raise_error(MTLibcouchbase::Error::KeyNotFound) + expect { parent.reload }.to raise_error(Couchbase::Error::DocumentNotFound) # Save will always return true unless the model is changed (won't touch the database) parent.name = 'should fail' - expect { parent.save }.to raise_error(MTLibcouchbase::Error::KeyNotFound) - expect { parent.save! }.to raise_error(MTLibcouchbase::Error::KeyNotFound) + expect { parent.save }.to raise_error(Couchbase::Error::DocumentNotFound) + expect { parent.save! }.to raise_error(Couchbase::Error::DocumentNotFound) end it "should cache associations" do diff --git a/spec/base_spec.rb b/spec/base_spec.rb index c061a45b..0a898c39 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -35,11 +35,11 @@ class CompareTest < CouchbaseOrm::Base it "should load database responses" do base = BaseTest.create!(name: 'joe') - resp = BaseTest.bucket.get(base.id, extended: true) + resp = BaseTest.bucket.default_collection.get(base.id) - expect(resp.key).to eq(base.id) + base_loaded = BaseTest.new(resp, id: base.id) - base_loaded = BaseTest.new(resp) + expect(base_loaded.id).to eq(base.id) expect(base_loaded).to eq(base) expect(base_loaded).not_to be(base) @@ -49,7 +49,7 @@ class CompareTest < CouchbaseOrm::Base it "should not load objects if there is a type mismatch" do base = BaseTest.create!(name: 'joe') - expect { CompareTest.find_by_id(base.id) }.to raise_error(RuntimeError) + expect { CompareTest.find_by_id(base.id) }.to raise_error(CouchbaseOrm::Error::TypeMismatchError) base.destroy end diff --git a/spec/collection_proxy_spec.rb b/spec/collection_proxy_spec.rb new file mode 100644 index 00000000..c6030968 --- /dev/null +++ b/spec/collection_proxy_spec.rb @@ -0,0 +1,29 @@ +require File.expand_path("../support", __FILE__) +require File.expand_path("../../lib/couchbase-orm/proxies/collection_proxy", __FILE__) + +class Proxyfied + def get(key, options = nil) + raise Couchbase::Error::DocumentNotFound + end + def remove(key, options = nil) + raise Couchbase::Error::DocumentNotFound + end +end + +describe CouchbaseOrm::CollectionProxy do + it "should raise an error when get is called with bang version" do + expect { CouchbaseOrm::CollectionProxy.new(Proxyfied.new).get!('key') }.to raise_error(Couchbase::Error::DocumentNotFound) + end + + it "should not raise an error when get is called with non bang version" do + expect { CouchbaseOrm::CollectionProxy.new(Proxyfied.new).get('key') }.to_not raise_error + end + + it "should raise an error when remove is called with bang version" do + expect { CouchbaseOrm::CollectionProxy.new(Proxyfied.new).remove!('key') }.to raise_error(Couchbase::Error::DocumentNotFound) + end + + it "should not raise an error when remove is called with non bang version" do + expect { CouchbaseOrm::CollectionProxy.new(Proxyfied.new).remove('key') }.to_not raise_error + end +end diff --git a/spec/has_many_spec.rb b/spec/has_many_spec.rb index 1e713305..b4069507 100644 --- a/spec/has_many_spec.rb +++ b/spec/has_many_spec.rb @@ -5,7 +5,6 @@ shared_examples "has_many example" do |parameter| before :all do @context = parameter[:context].to_s - @rating_test_class = Kernel.const_get("Rating#{@context.camelize}Test".classify) @object_test_class = Kernel.const_get("Object#{@context.camelize}Test".classify) @object_rating_test_class = Kernel.const_get("ObjectRating#{@context.camelize}Test".classify) @@ -13,12 +12,16 @@ @rating_test_class.ensure_design_document! @object_test_class.ensure_design_document! @object_rating_test_class.ensure_design_document! + + @rating_test_class.all.each(&:destroy) + @object_test_class.all.each(&:destroy) + @object_rating_test_class.all.each(&:destroy) end after :each do - @rating_test_class.all.stream(&:delete) - @object_test_class.all.stream(&:delete) - @object_rating_test_class.all.stream(&:delete) + @rating_test_class.all.each(&:destroy) + @object_test_class.all.each(&:destroy) + @object_rating_test_class.all.each(&:destroy) end it "should return matching results" do @@ -38,7 +41,7 @@ expect(docs).to match_array([1, 2]) first.destroy - expect { @rating_test_class.find rate.id }.to raise_error(::MTLibcouchbase::Error::KeyNotFound) + expect { @rating_test_class.find rate.id }.to raise_error(Couchbase::Error::DocumentNotFound) expect(@rating_test_class.all.count).to be(1) end diff --git a/spec/index_spec.rb b/spec/index_spec.rb index a4cb443a..cf4436e3 100644 --- a/spec/index_spec.rb +++ b/spec/index_spec.rb @@ -4,6 +4,7 @@ class IndexTest < CouchbaseOrm::Base + n1ql :all attribute :email, type: String attribute :name, type: String, default: :joe ensure_unique :email, presence: false @@ -18,13 +19,13 @@ class NoUniqueIndexTest < CouchbaseOrm::Base class EnumTest < CouchbaseOrm::Base enum visibility: [:group, :authority, :public], default: :authority + enum color: [:red, :green, :blue] end describe CouchbaseOrm::Index do after :each do - IndexTest.bucket.delete('index_testemail-joe@aca.com') - IndexTest.bucket.delete('index_testemail-') + IndexTest.all.map(&:destroy) end it "should prevent models being created if they should have unique keys" do @@ -98,6 +99,10 @@ class EnumTest < CouchbaseOrm::Base enum = EnumTest.create! expect(enum.visibility).to eq(2) enum.destroy + + # Test default default + enum = EnumTest.create! + expect(enum.color).to eq(1) end it "should not overwrite index's that do not belong to the current model" do diff --git a/spec/n1ql_spec.rb b/spec/n1ql_spec.rb index 66da5f8a..fd75180b 100644 --- a/spec/n1ql_spec.rb +++ b/spec/n1ql_spec.rb @@ -9,6 +9,12 @@ class N1QLTest < CouchbaseOrm::Base n1ql :all n1ql :by_name, emit_key: :name n1ql :by_rating, emit_key: :rating + n1ql :by_custom_rating, query_fn: proc { |bucket, _values, options| + cluster.query("SELECT raw meta().id FROM `#{bucket.name}` where type = 'n1_ql_test' AND rating IN [1,2] ORDER BY name ASC", options) + } + n1ql :by_custom_rating_values, emit_key: [:rating], query_fn: proc { |bucket, values, options| + cluster.query("SELECT raw meta().id FROM `#{bucket.name}` where type = 'n1_ql_test' AND rating IN #{values[0]} ORDER BY name ASC", options) + } # This generates both: # view :by_rating, emit_key: :rating # same as above @@ -58,6 +64,26 @@ class N1QLTest < CouchbaseOrm::Base expect(Set.new(docs)).to eq(Set.new(%w[bob jane])) end + it "should return matching results with custom n1ql query" do + N1QLTest.create! name: :bob, rating: :awesome + N1QLTest.create! name: :jane, rating: :awesome + N1QLTest.create! name: :greg, rating: :bad + N1QLTest.create! name: :mel, rating: :good + + + docs = N1QLTest.by_custom_rating().collect { |ob| + ob.name + } + + expect(Set.new(docs)).to eq(Set.new(%w[bob jane mel])) + + docs = N1QLTest.by_custom_rating_values(key: [[1, 2]]).collect { |ob| + ob.name + } + + expect(Set.new(docs)).to eq(Set.new(%w[bob jane mel])) + end + after(:all) do N1QLTest.all.to_a.each(&:destroy) end diff --git a/spec/persistence_spec.rb b/spec/persistence_spec.rb index 033b1972..71052bc3 100644 --- a/spec/persistence_spec.rb +++ b/spec/persistence_spec.rb @@ -205,11 +205,7 @@ class ModelWithValidations < CouchbaseOrm::Base expect(model.save!).to be(model) # coercion will fail here - begin - model.age = 'a23' - expect(false).to be(true) - rescue ArgumentError => e - end + expect{ model.age = "a23" }.to raise_error(ArgumentError) model.destroy end diff --git a/spec/support.rb b/spec/support.rb index 5c26d42a..68cec06a 100644 --- a/spec/support.rb +++ b/spec/support.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true, encoding: ASCII-8BIT - - -if ENV['TRAVIS_TEST'] - require 'mt-libcouchbase' - MTLibcouchbase::Defaults.username = 'tester' - MTLibcouchbase::Defaults.password = 'password123' -end - +require 'simplecov' require 'couchbase-orm' require 'minitest/assertions' require 'active_model/lint' +SimpleCov.start do + add_group 'Core', [/lib\/couchbase-orm\/(?!(proxies|utilities))/, 'lib/couchbase-orm.rb'] + add_group 'Proxies', 'lib/couchbase-orm/proxies' + add_group 'Utilities', 'lib/couchbase-orm/utilities' + add_group 'Specs', 'spec' + minimum_coverage 94 +end shared_examples_for "ActiveModel" do include Minitest::Assertions diff --git a/spec/views_spec.rb b/spec/views_spec.rb index 22ed7bb7..57af27a2 100644 --- a/spec/views_spec.rb +++ b/spec/views_spec.rb @@ -18,10 +18,27 @@ class ViewTest < CouchbaseOrm::Base describe CouchbaseOrm::Views do + before(:each) do + ViewTest.all.each(&:destroy) + rescue Couchbase::Error::DesignDocumentNotFound + # ignore (FIXME: check before merge) mainly because if there is nothing in all we should not have an error + end + + after(:each) do + ViewTest.all.each(&:destroy) + rescue Couchbase::Error::InternalServerFailure + # ignore (FIXME: check before merge) + rescue Couchbase::Error::DesignDocumentNotFound + # ignore (FIXME: check before merge) (7.1) + end + it "should save a new design document" do begin - ViewTest.bucket.delete_design_doc(ViewTest.design_document) - rescue MTLibcouchbase::Error::HttpResponseError + ViewTest.bucket.view_indexes.drop_design_document(ViewTest.design_document, :production) + rescue Couchbase::Error::InternalServerFailure + # ignore if design document does not exist + rescue Couchbase::Error::DesignDocumentNotFound + # ignore if design document does not exist (7.1) end expect(ViewTest.ensure_design_document!).to be(true) end @@ -29,6 +46,10 @@ class ViewTest < CouchbaseOrm::Base it "should not re-save a design doc if nothing has changed" do expect(ViewTest.ensure_design_document!).to be(false) end + + it "should return an empty array when there is no objects" do + expect(ViewTest.all).to eq([]) + end it "should perform a map-reduce and return the view" do ViewTest.ensure_design_document! @@ -47,7 +68,7 @@ class ViewTest < CouchbaseOrm::Base ViewTest.create! name: :jane, rating: :awesome ViewTest.create! name: :greg, rating: :bad - docs = ViewTest.by_rating(descending: :true).collect { |ob| + docs = ViewTest.by_rating(order: :descending).collect { |ob| ob.destroy ob.name } @@ -64,9 +85,6 @@ class ViewTest < CouchbaseOrm::Base docs = ViewTest.find_by_rating(1).collect { |ob| ob.name } - ViewTest.all.stream { |ob| - ob.destroy - } expect(Set.new(docs)).to eq(Set.new(['bob', 'jane'])) end