diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bbaff0ef..02685a0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: include: - ruby: '3.0' gemfile: '7.0.0' - couchbase: '6.6.5' + couchbase: '6.6.5' - ruby: '3.0' gemfile: '7.0.0' couchbase: '7.1.0' @@ -21,11 +21,11 @@ jobs: gemfile: '7.0.0' couchbase: '7.1.0' - ruby: '2.6' - gemfile: '5.1.7' + gemfile: '5.2.8.1' couchbase: '7.1.0' fail-fast: false runs-on: ubuntu-20.04 - name: ${{ matrix.ruby }} rails-${{ matrix.gemfile }} couchbase-${{ matrix.couchbase }} + 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 libev-dev python-httplib2 diff --git a/.gitignore b/.gitignore index a0219977..63dba921 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.sw? .DS_Store +.rbenv-vars coverage rdoc html diff --git a/couchbase-orm.gemspec b/couchbase-orm.gemspec index 79eba3a5..7e85f2f5 100644 --- a/couchbase-orm.gemspec +++ b/couchbase-orm.gemspec @@ -13,7 +13,9 @@ Gem::Specification.new do |gem| gem.required_ruby_version = '>= 2.1.0' gem.require_paths = ["lib"] - gem.add_runtime_dependency 'activemodel', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.0' + gem.add_runtime_dependency 'activemodel', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.2' + gem.add_runtime_dependency 'activerecord', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.2' + gem.add_runtime_dependency 'couchbase' gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base @@ -21,6 +23,7 @@ Gem::Specification.new do |gem| gem.add_development_dependency 'rspec', '~> 3.7' gem.add_development_dependency 'yard', '~> 0.9' gem.add_development_dependency 'pry' + gem.add_development_dependency 'pry-stack_explorer' gem.add_development_dependency 'simplecov' gem.files = `git ls-files`.split("\n") diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index 4c6257a6..fd573143 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -1,5 +1,10 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +require "active_support/lazy_load_hooks" +ActiveSupport.on_load(:i18n) do + I18n.load_path << File.expand_path("couchbase-orm/locale/en.yml", __dir__) +end + module CouchbaseOrm autoload :Error, 'couchbase-orm/error' autoload :Connection, 'couchbase-orm/connection' @@ -8,7 +13,7 @@ module CouchbaseOrm autoload :HasMany, 'couchbase-orm/utilities/has_many' def self.logger - @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT) + @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO unless ENV["COUCHBASE_ORM_DEBUG"] } end def self.logger=(logger) diff --git a/lib/couchbase-orm/associations.rb b/lib/couchbase-orm/associations.rb index 0d21f94c..71514d5b 100644 --- a/lib/couchbase-orm/associations.rb +++ b/lib/couchbase-orm/associations.rb @@ -114,9 +114,8 @@ def has_and_belongs_to_many(name, **options) old, new = previous_changes[ref] adds = (new || []) - (old || []) subs = (old || []) - (new || []) - - update_has_and_belongs_to_many_reverse_association(assoc, adds, true, **options) - update_has_and_belongs_to_many_reverse_association(assoc, subs, false, **options) + update_has_and_belongs_to_many_reverse_association(assoc, adds, true, **options) if adds.any? + update_has_and_belongs_to_many_reverse_association(assoc, subs, false, **options) if subs.any? end after_create save_method @@ -167,9 +166,11 @@ def update_has_and_belongs_to_many_reverse_association(assoc, keys, is_add, **op elsif !is_add && index tab = tab.dup tab.delete_at(index) + else + next end - v.__send__(:"#{remote_method}=", tab) - v.__send__(:save!) + v[remote_method] = tab + v.save! end end diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 185d7ad6..ea298952 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -2,6 +2,12 @@ require 'active_model' +require 'active_record' +if ActiveModel::VERSION::MAJOR >= 6 + require 'active_record/database_configurations' +else + require 'active_model/type' +end require 'active_support/hash_with_indifferent_access' require 'couchbase' require 'couchbase-orm/error' @@ -9,6 +15,7 @@ require 'couchbase-orm/n1ql' require 'couchbase-orm/persistence' require 'couchbase-orm/associations' +require 'couchbase-orm/types' require 'couchbase-orm/proxies/bucket_proxy' require 'couchbase-orm/proxies/collection_proxy' require 'couchbase-orm/utilities/join' @@ -19,17 +26,97 @@ module CouchbaseOrm + + module ActiveRecordCompat + # try to avoid dependencies on too many active record classes + # by exemple we don't want to go down to the concept of tables + + extend ActiveSupport::Concern + + module ClassMethods + def primary_key + "id" + end + + def base_class? + true + end + + def column_names # can't be an alias for now + attribute_names + end + + if ActiveModel::VERSION::MAJOR < 6 + def attribute_names + attribute_types.keys + end + + def abstract_class? + false + end + + def connected? + true + end + + def table_exists? + true + end + + # def partial_writes? + # partial_updates? && partial_inserts? + # end + end + end + + def _has_attribute?(attr_name) + attribute_names.include?(attr_name.to_s) + end + + def attribute_for_inspect(attr_name) + value = send(attr_name) + value.inspect + end + + if ActiveModel::VERSION::MAJOR < 6 + def attribute_names + self.class.attribute_names + end + + def has_attribute?(attr_name) + @attributes.key?(attr_name.to_s) + end + + def attribute_present?(attribute) + value = send(attribute) + !value.nil? && !(value.respond_to?(:empty?) && value.empty?) + end + + def _write_attribute(attr_name, value) + @attributes.write_from_user(attr_name.to_s, value) + value + end + end + end + class Base include ::ActiveModel::Model include ::ActiveModel::Dirty + include ::ActiveModel::Attributes include ::ActiveModel::Serializers::JSON include ::ActiveModel::Validations include ::ActiveModel::Validations::Callbacks + + include ::ActiveRecord::Core + include ActiveRecordCompat + define_model_callbacks :initialize, :only => :after define_model_callbacks :create, :destroy, :save, :update include Persistence + include ::ActiveRecord::AttributeMethods::Dirty + include ::ActiveRecord::Timestamp # must be included after Persistence include Associations include Views include N1ql @@ -73,33 +160,6 @@ def uuid_generator=(generator) @uuid_generator = generator end - def attribute(*names, **options) - @attributes ||= {} - names.each do |name| - name = name.to_sym - - @attributes[name] = options - - unless self.instance_methods.include?(name) - define_method(name) do - read_attribute(name) - end - end - - eq_meth = :"#{name}=" - unless self.instance_methods.include?(eq_meth) - define_method(eq_meth) do |value| - value = yield(value) if block_given? - write_attribute(name, value) - end - end - end - end - - def attributes - @attributes ||= {} - end - def find(*ids, quiet: false) CouchbaseOrm.logger.debug { "Base.find(l##{ids.length}) #{ids}" } @@ -124,7 +184,7 @@ def find_by_id(*ids, **options) alias_method :[], :find_by_id def exists?(id) - CouchbaseOrm.logger.debug "Data - Exists? #{id}" + CouchbaseOrm.logger.debug { "Data - Exists? #{id}" } collection.exists(id).exists end alias_method :has_key?, :exists? @@ -134,53 +194,37 @@ class MismatchTypeError < RuntimeError; end # Add support for libcouchbase response objects def initialize(model = nil, ignore_doc_type: false, **attributes) + CouchbaseOrm.logger.debug { "Initialize model #{model} with #{attributes.to_s.truncate(200)}" } @__metadata__ = Metadata.new - # Assign default values - @__attributes__ = ::ActiveSupport::HashWithIndifferentAccess.new({type: self.class.design_document}) - self.class.attributes.each do |key, options| - default = options[:default] - if default.respond_to?(:call) - write_attribute key, default.call - else - write_attribute key, default - end - end + super() if model case model when Couchbase::Collection::GetResult - CouchbaseOrm.logger.debug "Initialize with Couchbase::Collection::GetResult" - doc = model.content || raise('empty response provided') - type = doc.delete('type') + doc = HashWithIndifferentAccess.new(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 CouchbaseOrm::Error::TypeMismatchError.new("document type mismatch, #{type} != #{self.class.design_document}", self) end - @__metadata__.key = attributes[:id] + self.id = attributes[:id] if attributes[:id].present? @__metadata__.cas = model.cas - # This ensures that defaults are applied - @__attributes__.merge! doc + assign_attributes(doc) when CouchbaseOrm::Base - CouchbaseOrm.logger.debug "Initialize with CouchbaseOrm::Base" - clear_changes_information - attributes = model.attributes - attributes.delete(:id) - attributes.delete('type') - super(attributes) + super(model.attributes.except(:id, 'type')) else clear_changes_information - super(attributes.merge(Hash(model))) + assign_attributes(**attributes.merge(Hash(model)).symbolize_keys) end else clear_changes_information super(attributes) end - yield self if block_given? run_callbacks :initialize @@ -189,63 +233,23 @@ def initialize(model = nil, ignore_doc_type: false, **attributes) # Document ID is a special case as it is not stored in the document def id - @__metadata__.key || @id + @id end def id=(value) - raise 'ID cannot be changed' if @__metadata__.cas + raise 'ID cannot be changed' if @__metadata__.cas && value attribute_will_change!(:id) - @id = value.to_s + @id = value.to_s.presence end - def read_attribute(attr_name) - @__attributes__[attr_name] + def [](key) + send(key) end - alias_method :[], :read_attribute - def write_attribute(attr_name, value) - unless value.nil? - coerce = self.class.attributes[attr_name][:type] - value = Kernel.send(coerce.to_s, value) if coerce - end - attribute_will_change!(attr_name) unless @__attributes__[attr_name] == value - @__attributes__[attr_name] = value + def []=(key, value) + CouchbaseOrm.logger.debug { "Set attribute #{key} to #{value}" } + send(:"#{key}=", value) end - alias_method :[]=, :write_attribute - - # - # Add support for Serialization: - # http://guides.rubyonrails.org/active_model_basics.html#serialization - # - - def attributes - copy = @__attributes__.merge({id: id}) - copy.delete(:type) - copy - end - - def attributes=(attributes) - attributes.each do |key, value| - setter = :"#{key}=" - send(setter, value) if respond_to?(setter) - end - end - - ID_LOOKUP = ['id', :id].freeze - def attribute(name) - return self.id if ID_LOOKUP.include?(name) - @__attributes__[name] - end - alias_method :read_attribute_for_serialization, :attribute - - def attribute=(name, value) - __send__(:"#{name}=", value) - end - - - # - # Add support for comparisons - # # Public: Allows for access to ActiveModel functionality. # @@ -279,12 +283,7 @@ def eql?(other) # # Returns a boolean. def ==(other) - case other - when self.class - hash == other.hash - else - false - end + super || other.instance_of?(self.class) && !id.nil? && other.id == id end end end diff --git a/lib/couchbase-orm/locale/en.yml b/lib/couchbase-orm/locale/en.yml new file mode 100644 index 00000000..8e241faf --- /dev/null +++ b/lib/couchbase-orm/locale/en.yml @@ -0,0 +1,5 @@ +en: + couchbase: + errors: + messages: + record_invalid: "Validation failed: %{errors}" diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index 2f4562f7..857a42db 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -38,7 +38,7 @@ module ClassMethods 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? + raise "unknown emit_key attribute for n1ql :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s) end options = N1QL_DEFAULTS.merge(options) method_opts = {} @@ -49,9 +49,8 @@ def n1ql(name, query_fn: nil, emit_key: [], **options) singleton_class.__send__(:define_method, name) do |**opts, &result_modifier| opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus) - values = convert_values(opts.delete(:key)) + values = convert_values(method_opts[:emit_key], opts.delete(:key)) if opts[:key] current_query = run_query(method_opts[:emit_key], values, query_fn, **opts.except(:include_docs)) - if result_modifier opts[:include_docs] = true current_query.results &result_modifier @@ -73,23 +72,32 @@ def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil) validates(attr, presence: true) if validate n1ql n1ql_method, emit_key: attr - instance_eval " - def self.#{find_method}(#{attr}) - #{n1ql_method}(key: #{attr}) - end - " + define_singleton_method find_method do |value| + send n1ql_method, key: value + end end private - def convert_values(values) - Array.wrap(values).compact.map do |v| - if v.class == String - "'#{N1ql.sanitize(v)}'" - elsif v.class == Date || v.class == Time - "'#{v.iso8601(3)}'" + def convert_values(keys, values) + raise ArgumentError, "Empty keys but values are present, can't type cast" if keys.empty? && Array.wrap(values).any? + keys.zip(Array.wrap(values)).map do |key, value_before_type_cast| + # cast value to type + value = if value_before_type_cast.is_a?(Array) + value_before_type_cast.map do |v| + attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(v)) + end + else + attribute_types[key.to_s].serialize(attribute_types[key.to_s].cast(value_before_type_cast)) + end + + CouchbaseOrm.logger.debug { "convert_values: #{key} => #{value_before_type_cast.inspect} => #{value.inspect} #{value.class} #{attribute_types[key.to_s]}" } + + # then quote and sanitize + if value.class == String + "'#{N1ql.sanitize(value)}'" else - N1ql.sanitize(v).to_s + N1ql.sanitize(value).to_s end end end @@ -123,7 +131,7 @@ def run_query(keys, values, query_fn, descending: false, limit: nil, **options) 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" + CouchbaseOrm.logger.debug { "N1QL query: #{n1ql_query} return #{result.rows.to_a.length} rows" } N1qlProxy.new(result) end end diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 6e30e820..f3bc03cc 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -7,6 +7,9 @@ module CouchbaseOrm module Persistence extend ActiveSupport::Concern + included do + attribute :id, :string + end module ClassMethods def create(attributes = nil, &block) @@ -54,20 +57,19 @@ def inherited(child) # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the database yet; otherwise, returns false. def new_record? - @__metadata__.cas.nil? && @__metadata__.key.nil? + @__metadata__.cas.nil? && id.nil? end alias_method :new?, :new_record? # Returns true if this object has been destroyed, otherwise returns false. def destroyed? - !!(@__metadata__.cas && @__metadata__.key.nil?) + !!(@__metadata__.cas && id.blank?) end # Returns true if the record is persisted, i.e. it's not a new record and it was # not destroyed, otherwise returns false. def persisted? - # Changed? is provided by ActiveModel::Dirty - !!@__metadata__.key + id.present? end alias_method :exists?, :persisted? @@ -88,6 +90,7 @@ def save(**options) # By default, #save! always runs validations. If any of them fail # CouchbaseOrm::Error::RecordInvalid gets raised, and the record won't be saved. def save!(**options) + CouchbaseOrm.logger.debug { "Will save! : #{id} -> #{attributes.to_s.truncate(200)}" } self.class.fail_validate!(self) unless self.save(**options) self end @@ -99,12 +102,10 @@ def save!(**options) # The record is simply removed, no callbacks are executed. def delete(with_cas: false, **options) options[:cas] = @__metadata__.cas if with_cas - CouchbaseOrm.logger.debug "Data - Delete #{@__metadata__.key}" - self.class.collection.remove(@__metadata__.key, **options) - - @__metadata__.key = nil - @id = nil + CouchbaseOrm.logger.debug "Data - Delete #{self.id}" + self.class.collection.remove(self.id, **options) + self.id = nil clear_changes_information self.freeze self @@ -124,11 +125,10 @@ def destroy(with_cas: false, **options) destroy_associations! options[:cas] = @__metadata__.cas if with_cas - CouchbaseOrm.logger.debug "Data - Delete #{@__metadata__.key}" - self.class.collection.remove(@__metadata__.key, **options) - - @__metadata__.key = nil - @id = nil + CouchbaseOrm.logger.debug "Data - Destroy #{id}" + self.class.collection.remove(id, **options) + + self.id = nil clear_changes_information freeze @@ -146,6 +146,10 @@ def update_attribute(name, value) changed? ? save(validate: false) : true end + def assign_attributes(hash) + super(hash.with_indifferent_access.except("type")) + end + # Updates the attributes of the model from the passed-in hash and saves the # record. If the object is invalid, the saving will fail and false will be returned. def update(hash) @@ -167,8 +171,7 @@ def update!(hash) # 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 + raise "unable to update columns, model not persisted" unless id assign_attributes(hash) @@ -178,15 +181,13 @@ def update_columns(with_cas: false, **hash) # There is a limit of 16 subdoc operations per request resp = if hash.length <= 16 self.class.collection.mutate_in( - _id, + 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.collection.replace(_id, @__attributes__, **options) + CouchbaseOrm.logger.debug { "Data - Replace #{id} #{attributes.to_s.truncate(200)}" } + self.class.collection.replace(id, attributes.except(:id).merge(type: self.class.design_document), **options) end # Ensure the model is up to date @@ -200,13 +201,11 @@ def update_columns(with_cas: false, **hash) # # This method finds record by its key and modifies the receiver in-place: def reload - key = @__metadata__.key - raise "unable to reload, model not persisted" unless key + raise "unable to reload, model not persisted" unless id - CouchbaseOrm.logger.debug "Data - Get #{key}" - resp = self.class.collection.get!(key) - @__attributes__ = ::ActiveSupport::HashWithIndifferentAccess.new(resp.content) - @__metadata__.key = key + CouchbaseOrm.logger.debug "Data - Get #{id}" + resp = self.class.collection.get!(id) + assign_attributes(resp.content.except("id")) # API return a nil id @__metadata__.cas = resp.cas reset_associations @@ -216,8 +215,8 @@ def reload # Updates the TTL of the document def touch(**options) - CouchbaseOrm.logger.debug "Data - Touch #{@__metadata__.key}" - res = self.class.collection.touch(@__metadata__.key, async: false, **options) + CouchbaseOrm.logger.debug "Data - Touch #{id}" + _res = self.class.collection.touch(id, async: false, **options) @__metadata__.cas = resp.cas self end @@ -225,24 +224,23 @@ def touch(**options) protected - - def _update_record(with_cas: false, **options) + def serialized_attributes + attributes.map { |k, v| + [k, self.class.attribute_types[k].serialize(v)] + }.to_h + end + + def _update_record(*_args, with_cas: false, **options) return false unless perform_validations(:update, options) return true unless changed? run_callbacks :update do run_callbacks :save do - # Ensure the type is set - @__attributes__[:type] = self.class.design_document - @__attributes__.delete(:id) - - _id = @__metadata__.key options[:cas] = @__metadata__.cas if with_cas - CouchbaseOrm.logger.debug { "_update_record - replace #{_id} #{@__attributes__.to_s.truncate(200)}" } - resp = self.class.collection.replace(_id, @__attributes__, Couchbase::Options::Replace.new(**options)) + CouchbaseOrm.logger.debug { "_update_record - replace #{id} #{serialized_attributes.to_s.truncate(200)}" } + resp = self.class.collection.replace(id, serialized_attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Replace.new(**options)) # Ensure the model is up to date - @__metadata__.key = _id @__metadata__.cas = resp.cas changes_applied @@ -250,23 +248,17 @@ def _update_record(with_cas: false, **options) end end end - def _create_record(**options) + def _create_record(*_args, **options) return false unless perform_validations(:create, options) run_callbacks :create do run_callbacks :save do - # Ensure the type is set - @__attributes__[:type] = self.class.design_document - @__attributes__.delete(:id) - - _id = @id || self.class.uuid_generator.next(self) - CouchbaseOrm.logger.debug { "_create_record - Upsert #{_id} #{@__attributes__.to_s.truncate(200)}" } - #resp = self.class.collection.add(_id, @__attributes__, **options) + assign_attributes(id: self.class.uuid_generator.next(self)) unless self.id + CouchbaseOrm.logger.debug { "_create_record - Upsert #{id} #{serialized_attributes.to_s.truncate(200)}" } - resp = self.class.collection.upsert(_id, @__attributes__, Couchbase::Options::Upsert.new(**options)) + resp = self.class.collection.upsert(self.id, serialized_attributes.except(:id).merge(type: self.class.design_document), Couchbase::Options::Upsert.new(**options)) # Ensure the model is up to date - @__metadata__.key = _id @__metadata__.cas = resp.cas changes_applied diff --git a/lib/couchbase-orm/proxies/n1ql_proxy.rb b/lib/couchbase-orm/proxies/n1ql_proxy.rb index 62a8ab3b..db4e9363 100644 --- a/lib/couchbase-orm/proxies/n1ql_proxy.rb +++ b/lib/couchbase-orm/proxies/n1ql_proxy.rb @@ -12,7 +12,7 @@ def initialize(proxyfied) @current_query = self.to_s return @results if @results - CouchbaseOrm.logger.debug 'Query - ' + self.to_s + CouchbaseOrm.logger.debug { 'Query - ' + self.to_s } results = @proxyfied.rows results = results.map { |r| block.call(r) } if block diff --git a/lib/couchbase-orm/types.rb b/lib/couchbase-orm/types.rb new file mode 100644 index 00000000..e6674c0d --- /dev/null +++ b/lib/couchbase-orm/types.rb @@ -0,0 +1,14 @@ +require "couchbase-orm/types/date" +require "couchbase-orm/types/date_time" +require "couchbase-orm/types/timestamp" + +if ActiveModel::VERSION::MAJOR < 6 + # In Rails 5, the type system cannot allow overriding the default types + ActiveModel::Type.registry.instance_variable_get(:@registrations).delete_if do |k| + k.matches?(:date) || k.matches?(:datetime) || k.matches?(:timestamp) + end +end + +ActiveModel::Type.register(:date, CouchbaseOrm::Types::Date) +ActiveModel::Type.register(:datetime, CouchbaseOrm::Types::DateTime) +ActiveModel::Type.register(:timestamp, CouchbaseOrm::Types::Timestamp) diff --git a/lib/couchbase-orm/types/date.rb b/lib/couchbase-orm/types/date.rb new file mode 100644 index 00000000..2e836cf8 --- /dev/null +++ b/lib/couchbase-orm/types/date.rb @@ -0,0 +1,9 @@ +module CouchbaseOrm + module Types + class Date < ActiveModel::Type::Date + def serialize(value) + value&.iso8601 + end + end + end +end diff --git a/lib/couchbase-orm/types/date_time.rb b/lib/couchbase-orm/types/date_time.rb new file mode 100644 index 00000000..a9a26984 --- /dev/null +++ b/lib/couchbase-orm/types/date_time.rb @@ -0,0 +1,13 @@ +module CouchbaseOrm + module Types + class DateTime < ActiveModel::Type::DateTime + def cast(value) + super(value)&.utc + end + + def serialize(value) + value&.iso8601 + end + end + end +end diff --git a/lib/couchbase-orm/types/timestamp.rb b/lib/couchbase-orm/types/timestamp.rb new file mode 100644 index 00000000..51f8acbc --- /dev/null +++ b/lib/couchbase-orm/types/timestamp.rb @@ -0,0 +1,18 @@ +module CouchbaseOrm + module Types + class Timestamp < ActiveModel::Type::DateTime + def cast(value) + return nil if value.nil? + return Time.at(value) if value.is_a?(Integer) + return Time.at(value.to_i) if value.is_a?(String) && value =~ /^[0-9]+$/ + return value.utc if value.is_a?(Time) + super(value).utc + end + + def serialize(value) + value&.to_i + end + end + end +end + diff --git a/lib/couchbase-orm/utilities/enum.rb b/lib/couchbase-orm/utilities/enum.rb index 87d1fa1d..6032f90c 100644 --- a/lib/couchbase-orm/utilities/enum.rb +++ b/lib/couchbase-orm/utilities/enum.rb @@ -27,7 +27,19 @@ def enum(options) else default_value = 1 end - attribute name, default: default_value + attribute name, :integer, default: default_value + + define_method "#{name}=" do |value| + unless value.nil? + value = case value + when Symbol, String + self.class.const_get(name.to_s.upcase)[value.to_sym] + else + Integer(value) + end + end + super(value) + end # keep the attribute's value within bounds before_save do |record| diff --git a/lib/couchbase-orm/utilities/has_many.rb b/lib/couchbase-orm/utilities/has_many.rb index d03458b0..9f43613d 100644 --- a/lib/couchbase-orm/utilities/has_many.rb +++ b/lib/couchbase-orm/utilities/has_many.rb @@ -37,6 +37,7 @@ class #{class_name} < CouchbaseOrm::Base return self.instance_variable_get(instance_var) if instance_variable_defined?(instance_var) remote_klass = remote_class.constantize + raise ArgumentError, "Can't find #{remote_method} without an id" unless self.id.present? enum = klass.__send__(remote_method, key: self.id) { |row| case type when :n1ql @@ -93,7 +94,8 @@ 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_fn: proc { |bucket, values, options| + n1ql remote_method, emit_key: 'id', query_fn: proc { |bucket, values, options| + raise ArgumentError, "values[0] must not be blank" if values[0].blank? cluster.query("SELECT raw #{through_key} FROM `#{bucket.name}` where type = \"#{design_document}\" and #{foreign_key} = #{values[0]}", options) } end @@ -103,5 +105,5 @@ def build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_ke end end end - end + end end diff --git a/lib/couchbase-orm/utilities/index.rb b/lib/couchbase-orm/utilities/index.rb index f12d0ecd..0df9ef5c 100644 --- a/lib/couchbase-orm/utilities/index.rb +++ b/lib/couchbase-orm/utilities/index.rb @@ -31,7 +31,7 @@ def index(attrs, name = nil, presence: true, &processor) # collect a list of values for each key component attribute define_method(bucket_key_vals_method) do - attrs.collect {|attr| self[attr]} + attrs.collect {|attr| self.class.attribute_types[attr.to_s].cast(self[attr])} end @@ -40,6 +40,7 @@ def index(attrs, name = nil, presence: true, &processor) #---------------- # simple wrapper around the processor proc if supplied define_singleton_method(processor_method) do |*values| + values = attrs.zip(values).map { |attr,value| attribute_types[attr.to_s].serialize(attribute_types[attr.to_s].cast(value)) } if processor processor.call(values.length == 1 ? values.first : values) else @@ -50,6 +51,7 @@ 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) + CouchbaseOrm.logger.debug { "#{find_by_method}: #{class_bucket_key_method} with values #{values.inspect} give key: #{key}" } id = self.collection.get(key)&.content if id mod = self.find_by_id(id) @@ -57,6 +59,8 @@ def index(attrs, name = nil, presence: true, &processor) # Clean up record if the id doesn't exist self.collection.remove(key) + else + CouchbaseOrm.logger.debug("#{find_by_method}: #{key} not found") end nil @@ -70,7 +74,7 @@ def index(attrs, name = nil, presence: true, &processor) if presence attrs.each do |attr| validates attr, presence: true - define_attribute_methods attr + attribute attr end end @@ -103,7 +107,7 @@ def index(attrs, name = nil, presence: true, &processor) 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}" + CouchbaseOrm.logger.debug { "Removing old key #{original_key}" } record.class.collection.remove(original_key, cas: check_ref_id.cas) end end diff --git a/lib/couchbase-orm/views.rb b/lib/couchbase-orm/views.rb index 530554cf..98399954 100644 --- a/lib/couchbase-orm/views.rb +++ b/lib/couchbase-orm/views.rb @@ -25,10 +25,10 @@ module ClassMethods def view(name, map: nil, emit_key: nil, reduce: nil, **options) if emit_key.class == Array emit_key.each do |key| - raise "unknown emit_key attribute for view :#{name}, emit_key: :#{key}" if key && @attributes[key].nil? + raise "unknown emit_key attribute for view :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s) end else - raise "unknown emit_key attribute for view :#{name}, emit_key: :#{emit_key}" if emit_key && @attributes[emit_key].nil? + raise "unknown emit_key attribute for view :#{name}, emit_key: :#{emit_key}" if emit_key && !attribute_names.include?(emit_key.to_s) end options = ViewDefaults.merge(options) @@ -48,27 +48,13 @@ def view(name, map: nil, emit_key: nil, reduce: nil, **options) EMAP else emit_key = emit_key || :created_at - - if emit_key != :created_at && self.attributes[emit_key][:type].to_s == 'Array' - method_opts[:map] = <<-EMAP -function(doc) { - var i; - if (doc.type === "{{design_document}}") { - for (i = 0; i < doc.#{emit_key}.length; i += 1) { - emit(doc.#{emit_key}[i], null); - } - } -} -EMAP - else - method_opts[:map] = <<-EMAP + method_opts[:map] = <<-EMAP function(doc) { if (doc.type === "{{design_document}}") { emit(doc.#{emit_key}, null); } } EMAP - end end end diff --git a/spec/base_spec.rb b/spec/base_spec.rb index 0a898c39..ae05ee75 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -4,14 +4,17 @@ class BaseTest < CouchbaseOrm::Base - attribute :name, :job + attribute :name, :string + attribute :job, :string end class CompareTest < CouchbaseOrm::Base - attribute :age + attribute :age, :integer end - +class TimestampTest < CouchbaseOrm::Base + attribute :created_at, :datetime +end describe CouchbaseOrm::Base do it "should be comparable to other objects" do @@ -33,6 +36,11 @@ class CompareTest < CouchbaseOrm::Base base3.delete end + it "should be inspectable" do + base = BaseTest.create!(name: 'joe') + expect(base.inspect).to eq("#") + end + it "should load database responses" do base = BaseTest.create!(name: 'joe') resp = BaseTest.bucket.default_collection.get(base.id) @@ -58,7 +66,7 @@ class CompareTest < CouchbaseOrm::Base base = BaseTest.create!(name: 'joe') base_id = base.id - expect(base.to_json).to eq({name: 'joe', job: nil, id: base_id}.to_json) + expect(base.to_json).to eq({id: base_id, name: 'joe', job: nil}.to_json) expect(base.to_json(only: :name)).to eq({name: 'joe'}.to_json) base.destroy @@ -73,6 +81,12 @@ class CompareTest < CouchbaseOrm::Base base.name = 'change' expect(base.changes.empty?).to be(false) + # Attributes are set by key + base = BaseTest.new + base[:name] = 'bob' + expect(base.changes.empty?).to be(false) + + # Attributes are set by initializer from hash base = BaseTest.new({name: 'bob'}) expect(base.changes.empty?).to be(false) expect(base.previous_changes.empty?).to be(true) @@ -80,12 +94,12 @@ class CompareTest < CouchbaseOrm::Base # A saved model should have no changes base = BaseTest.create!(name: 'joe') expect(base.changes.empty?).to be(true) - expect(base.previous_changes.empty?).to be(false) # Attributes are copied from the existing model base = BaseTest.new(base) expect(base.changes.empty?).to be(false) expect(base.previous_changes.empty?).to be(true) + ensure base.destroy if base.id end @@ -121,6 +135,31 @@ class CompareTest < CouchbaseOrm::Base end end + it "should set the attribute on creation" do + base = BaseTest.create!(name: 'joe') + expect(base.name).to eq('joe') + ensure + base.destroy + end + + it "should support getting the attribute by key" do + base = BaseTest.create!(name: 'joe') + expect(base[:name]).to eq('joe') + ensure + base.destroy + end + + if ActiveModel::VERSION::MAJOR >= 6 + it "should have timestamp attributes for create in model" do + expect(TimestampTest.timestamp_attributes_for_create_in_model).to eq(["created_at"]) + end + end + + it "should generate a timestamp on creation" do + base = TimestampTest.create!() + expect(base.created_at).to be_a(Time) + end + describe BaseTest do it_behaves_like "ActiveModel" end diff --git a/spec/enum_spec.rb b/spec/enum_spec.rb new file mode 100644 index 00000000..76036183 --- /dev/null +++ b/spec/enum_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true, encoding: ASCII-8BIT + +require File.expand_path("../support", __FILE__) + +class EnumTest < CouchbaseOrm::Base + enum rating: [:awesome, :good, :okay, :bad], default: :okay + enum color: [:red, :green, :blue] +end + +describe CouchbaseOrm::Base do + it "should create an attribute" do + base = EnumTest.create!(rating: :good, color: :red) + expect(base.attribute_names).to eq(["id", "rating", "color"]) + end + + it "should set the attribute" do + base = EnumTest.create!(rating: :good, color: :red) + expect(base.rating).to_not be_nil + expect(base.color).to_not be_nil + end + + it "should convert it to an int" do + base = EnumTest.create!(rating: :good, color: :red) + expect(base.rating).to eq 2 + expect(base.color).to eq 1 + end + + it "should use default value" do + base = EnumTest.create! + expect(base.rating).to eq 3 + expect(base.color).to eq 1 + end +end + diff --git a/spec/has_many_spec.rb b/spec/has_many_spec.rb index b4069507..c03e4f32 100644 --- a/spec/has_many_spec.rb +++ b/spec/has_many_spec.rb @@ -50,8 +50,8 @@ second = @object_test_class.create! name: :jane rate1 = @rating_test_class.create! rating: :awesome, "object_#{@context}_test": first - rate2 = @rating_test_class.create! rating: :bad, "object_#{@context}_test": second - rate3 = @rating_test_class.create! rating: :good, "object_#{@context}_test": first + _rate2 = @rating_test_class.create! rating: :bad, "object_#{@context}_test": second + _rate3 = @rating_test_class.create! rating: :good, "object_#{@context}_test": first ort = @object_rating_test_class.create! "object_#{@context}_test": first, "rating_#{@context}_test": rate1 @object_rating_test_class.create! "object_#{@context}_test": second, "rating_#{@context}_test": rate1 diff --git a/spec/index_spec.rb b/spec/index_spec.rb index cf4436e3..8a724500 100644 --- a/spec/index_spec.rb +++ b/spec/index_spec.rb @@ -17,7 +17,7 @@ class NoUniqueIndexTest < CouchbaseOrm::Base index :email, presence: false end -class EnumTest < CouchbaseOrm::Base +class IndexEnumTest < CouchbaseOrm::Base enum visibility: [:group, :authority, :public], default: :authority enum color: [:red, :green, :blue] end @@ -86,22 +86,22 @@ class EnumTest < CouchbaseOrm::Base it "should work with enumerators" do # Test symbol - enum = EnumTest.create!(visibility: :public) + enum = IndexEnumTest.create!(visibility: :public) expect(enum.visibility).to eq(3) enum.destroy # Test number - enum = EnumTest.create!(visibility: 2) + enum = IndexEnumTest.create!(visibility: 2) expect(enum.visibility).to eq(2) enum.destroy # Test default - enum = EnumTest.create! + enum = IndexEnumTest.create! expect(enum.visibility).to eq(2) enum.destroy # Test default default - enum = EnumTest.create! + enum = IndexEnumTest.create! expect(enum.color).to eq(1) end diff --git a/spec/persistence_spec.rb b/spec/persistence_spec.rb index 71052bc3..6c83212a 100644 --- a/spec/persistence_spec.rb +++ b/spec/persistence_spec.rb @@ -4,7 +4,9 @@ class BasicModel < CouchbaseOrm::Base - attribute :name, :address, :age + attribute :name + attribute :address + attribute :age end class ModelWithDefaults < CouchbaseOrm::Base @@ -14,7 +16,9 @@ class ModelWithDefaults < CouchbaseOrm::Base end class ModelWithCallbacks < CouchbaseOrm::Base - attribute :name, :address, :age + attribute :name + attribute :address + attribute :age before_create :update_name before_save :set_address @@ -32,7 +36,8 @@ def set_age; self.age = 30; end end class ModelWithValidations < CouchbaseOrm::Base - attribute :name, :address, type: String + attribute :name, type: String + attribute :address, type: String attribute :age, type: :Integer validates :name, presence: true @@ -205,7 +210,8 @@ class ModelWithValidations < CouchbaseOrm::Base expect(model.save!).to be(model) # coercion will fail here - expect{ model.age = "a23" }.to raise_error(ArgumentError) + model.age = "a23" + expect{ model.save! }.to raise_error(CouchbaseOrm::Error::RecordInvalid) model.destroy end @@ -224,7 +230,7 @@ class ModelWithValidations < CouchbaseOrm::Base model.reload expect(model.changed?).to be(false) - expect(model.id).to be(id) + expect(model.id).to eq(id) model.destroy expect(model.destroyed?).to be(true) diff --git a/spec/support.rb b/spec/support.rb index 68cec06a..fe647064 100644 --- a/spec/support.rb +++ b/spec/support.rb @@ -3,6 +3,8 @@ require 'couchbase-orm' require 'minitest/assertions' require 'active_model/lint' +require 'pry' +require 'pry-stack_explorer' SimpleCov.start do add_group 'Core', [/lib\/couchbase-orm\/(?!(proxies|utilities))/, 'lib/couchbase-orm.rb'] diff --git a/spec/type_spec.rb b/spec/type_spec.rb new file mode 100644 index 00000000..e66a2efe --- /dev/null +++ b/spec/type_spec.rb @@ -0,0 +1,298 @@ +require File.expand_path("../support", __FILE__) + +require "active_model" +require "couchbase-orm/types" + +class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime + def serialize(value) + value&.iso8601(3) + end +end + +ActiveModel::Type.register(:datetime3decimal, DateTimeWith3Decimal) + +class TypeTest < CouchbaseOrm::Base + attribute :name, :string + attribute :age, :integer + attribute :size, :float + attribute :renewal_date, :date + attribute :subscribed_at, :datetime + attribute :some_time, :timestamp + attribute :precision_time, :datetime3decimal + attribute :active, :boolean + + n1ql :all + + index :age, presence: false + index :renewal_date, presence: false + index :some_time, presence: false + index :precision_time, presence: false +end + +class N1qlTypeTest < CouchbaseOrm::Base + attribute :name, :string + attribute :age, :integer + attribute :size, :float + attribute :renewal_date, :date + attribute :subscribed_at, :datetime + attribute :some_time, :timestamp + attribute :precision_time, :datetime3decimal + attribute :active, :boolean + + n1ql :all + + index_n1ql :name, validate: false + index_n1ql :age, validate: false + index_n1ql :size, validate: false + index_n1ql :active, validate: false + index_n1ql :renewal_date, validate: false + index_n1ql :some_time, validate: false + index_n1ql :subscribed_at, validate: false + index_n1ql :precision_time, validate: false + n1ql :by_both_dates, emit_key: [:renewal_date, :subscribed_at], presence: false +end + +TypeTest.ensure_design_document! +N1qlTypeTest.ensure_design_document! + +describe CouchbaseOrm::Types::Timestamp do + it "should cast an integer to time" do + t = Time.at(Time.now.to_i) + expect(CouchbaseOrm::Types::Timestamp.new.cast(t.to_i)).to eq(t) + end + it "should cast an integer string to time" do + t = Time.at(Time.now.to_i) + expect(CouchbaseOrm::Types::Timestamp.new.cast(t.to_s)).to eq(t) + end +end + +describe CouchbaseOrm::Types::Date do + it "should cast an string to date" do + d = Date.today + expect(CouchbaseOrm::Types::Date.new.cast(d.to_s)).to eq(d) + end + + it "should serialize date to string" do + d = Date.today + expect(CouchbaseOrm::Types::Date.new.serialize(d)).to eq(d.to_s) + end + + it "should get the type from the registry" do + expect(ActiveModel::Type.lookup(:date)).to eq(CouchbaseOrm::Types::Date.new) + end +end + +describe CouchbaseOrm::Base do + before(:each) do + TypeTest.all.each(&:destroy) + N1qlTypeTest.all.each(&:destroy) + end + + it "should be createable" do + t = TypeTest.create! + expect(t).to be_a(TypeTest) + end + + it "should be able to set attributes" do + t = TypeTest.new + t.name = "joe" + t.age = 20 + t.size = 1.5 + t.renewal_date = Date.today + t.subscribed_at = Time.now + t.active = true + t.save! + + expect(t.name).to eq("joe") + expect(t.age).to eq(20) + expect(t.size).to eq(1.5) + expect(t.renewal_date).to eq(Date.today) + expect(t.subscribed_at).to be_a(Time) + expect(t.active).to eq(true) + end + + it "should be able to set attributes with a hash" do + t = TypeTest.new(name: "joe", age: 20, size: 1.5, renewal_date: Date.today, subscribed_at: Time.now, active: true) + t.save! + + expect(t.name).to eq("joe") + expect(t.age).to eq(20) + expect(t.size).to eq(1.5) + expect(t.renewal_date).to eq(Date.today) + expect(t.subscribed_at).to be_a(Time) + expect(t.active).to eq(true) + end + + it "should be able to be stored and retrieved" do + now = Time.now + t = TypeTest.create!(name: "joe", age: 20, size: 1.5, renewal_date: Date.today, subscribed_at: now, active: true) + t2 = TypeTest.find(t.id) + + expect(t2.name).to eq("joe") + expect(t2.age).to eq(20) + expect(t2.size).to eq(1.5) + expect(t2.renewal_date).to eq(Date.today) + expect(t2.subscribed_at).to eq(now.utc.change(usec: 0)) + expect(t2.active).to eq(true) + end + + it "should be able to query by age" do + t = TypeTest.create!(age: 20) + _t2 = TypeTest.create!(age: 40) + expect(TypeTest.find_by_age(20)).to eq t + end + + it "should be able to query by age and type cast" do + t = TypeTest.create!(age: "20") + expect(TypeTest.find_by_age(20)).to eq t + expect(TypeTest.find_by_age("20")).to eq t + end + + it "should be able to query by date" do + t = TypeTest.create!(renewal_date: Date.today) + _t2 = TypeTest.create!(renewal_date: Date.today + 1) + expect(TypeTest.find_by_renewal_date(Date.today)).to eq t + end + + it "should be able to query by date and type cast" do + t = TypeTest.create!(renewal_date: Date.today.to_s) + expect(TypeTest.find_by_renewal_date(Date.today)).to eq t + expect(TypeTest.find_by_renewal_date(Date.today.to_s)).to eq t + end + + it "should be able to query by time" do + now = Time.now + t = TypeTest.create!(name: "t", some_time: now) + _t2 = TypeTest.create!(name: "t2", some_time: now + 1) + expect(TypeTest.find_by_some_time(now)).to eq t + end + + it "should be able to query by time and type cast" do + now = Time.now + now_s = now.to_i.to_s + t = TypeTest.create!(some_time: now_s) + expect(TypeTest.find_by_some_time(now)).to eq t + expect(TypeTest.find_by_some_time(now_s)).to eq t + end + + it "should be able to query by custom type" do + now = Time.now + t = TypeTest.create!(precision_time: now) + _t2 = TypeTest.create!(precision_time: now + 1) + expect(TypeTest.find_by_precision_time(now)).to eq t + end + + it "should be able to query by custom type and type cast" do + now = Time.now + now_s = now.utc.iso8601(3) + t = TypeTest.create!(precision_time: now_s) + expect(TypeTest.find_by_precision_time(now)).to eq t + expect(TypeTest.find_by_precision_time(now_s)).to eq t + end + + it "should be able to set attributes with a hash with indifferent access" do + t = TypeTest.new(ActiveSupport::HashWithIndifferentAccess.new(name: "joe", age: 20, size: 1.5, renewal_date: Date.today, subscribed_at: Time.now, active: true)) + t.save! + + expect(t.name).to eq("joe") + expect(t.age).to eq(20) + expect(t.size).to eq(1.5) + expect(t.renewal_date).to eq(Date.today) + expect(t.subscribed_at).to be_a(Time) + expect(t.active).to eq(true) + end + + it "should be able to type cast attributes" do + t = TypeTest.new(name: "joe", age: "20", size: "1.5", renewal_date: Date.today.to_s, subscribed_at: Time.now.to_s, active: "true") + t.save! + + expect(t.name).to eq("joe") + expect(t.age).to eq(20) + expect(t.size).to eq(1.5) + expect(t.renewal_date).to eq(Date.today) + expect(t.subscribed_at).to be_a(Time) + expect(t.active).to eq(true) + end + + it "should be consistent with active record on failed cast" do + t = TypeTest.new(name: "joe", age: "joe", size: "joe", renewal_date: "joe", subscribed_at: "joe", active: "true") + t.save! + + expect(t.age).to eq 0 + expect(t.size).to eq 0.0 + expect(t.renewal_date).to eq nil + expect(t.subscribed_at).to eq nil + expect(t.active).to eq true + end + + it "should be able to query by name" do + t = N1qlTypeTest.create!(name: "joe") + _t2 = N1qlTypeTest.create!(name: "john") + expect(N1qlTypeTest.find_by_name("joe").to_a).to eq [t] + end + + pending "should be able to query by nil value" do + t = N1qlTypeTest.create!() + _t2 = N1qlTypeTest.create!(name: "john") + expect(N1qlTypeTest.find_by_name(nil).to_a).to eq [t] + end + + pending "should be able to query by array value" do + t = N1qlTypeTest.create!(name: "laura") + t2 = N1qlTypeTest.create!(name: "joe") + _t3 = N1qlTypeTest.create!(name: "john") + expect(N1qlTypeTest.find_by_name(["laura", "joe"]).to_a).to match_array [t, t2] + end + + it "should be able to query by integer" do + t = N1qlTypeTest.create!(age: 20) + t2 = N1qlTypeTest.create!(age: 20) + _t3 = N1qlTypeTest.create!(age: 40) + expect(N1qlTypeTest.find_by_age(20).to_a).to match_array [t, t2] + end + + it "should be able to query by integer and type cast" do + t = N1qlTypeTest.create!(age: "20") + expect(N1qlTypeTest.find_by_age(20).to_a).to eq [t] + expect(N1qlTypeTest.find_by_age("20").to_a).to eq [t] + end + + it "should be able to query by date" do + t = N1qlTypeTest.create!(renewal_date: Date.today) + _t2 = N1qlTypeTest.create!(renewal_date: Date.today + 1) + expect(N1qlTypeTest.find_by_renewal_date(Date.today).to_a).to eq [t] + end + + it "should be able to query by datetime" do + now = Time.now + t = N1qlTypeTest.create!(subscribed_at: now) + _t2 = N1qlTypeTest.create!(subscribed_at: now + 1) + expect(N1qlTypeTest.find_by_subscribed_at(now).to_a).to eq [t] + end + + it "should be able to query by timestamp" do + now = Time.now + t = N1qlTypeTest.create!(some_time: now) + _t2 = N1qlTypeTest.create!(some_time: now + 1) + expect(N1qlTypeTest.find_by_some_time(now).to_a).to eq [t] + end + + it "should be able to query by custom type" do + now = Time.now + t = N1qlTypeTest.create!(precision_time: now) + _t2 = N1qlTypeTest.create!(precision_time: now + 1) + expect(N1qlTypeTest.find_by_precision_time(now).to_a).to eq [t] + end + + it "should be able to query by boolean" do + t = N1qlTypeTest.create!(active: true) + _t2 = N1qlTypeTest.create!(active: false) + expect(N1qlTypeTest.find_by_active(true).to_a).to eq [t] + end + + it "should be able to query by float" do + t = N1qlTypeTest.create!(size: 1.5) + _t2 = N1qlTypeTest.create!(size: 2.5) + expect(N1qlTypeTest.find_by_size(1.5).to_a).to eq [t] + end +end diff --git a/spec/views_spec.rb b/spec/views_spec.rb index 57af27a2..08ddb768 100644 --- a/spec/views_spec.rb +++ b/spec/views_spec.rb @@ -53,7 +53,7 @@ class ViewTest < CouchbaseOrm::Base it "should perform a map-reduce and return the view" do ViewTest.ensure_design_document! - mod = ViewTest.create! name: :bob, rating: :good + ViewTest.create! name: :bob, rating: :good docs = ViewTest.all.collect { |ob| ob.destroy