diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 00000000..776e6b48 --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,20 @@ +name: Linters + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + rubocop: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - name: Set up ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: 2.7 + - name: Run rubocop + run: bundle exec rubocop \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7fe6d51d..9ddf2573 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ vendor couchbase *.gem +rubocop_cache \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..cdc2aa5c --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,17 @@ +inherit_from: .rubocop_todo.yml + +inherit_gem: + mapotempo_rubocop: + - rubocop-default.yml + +require: + - rubocop-rspec + - rubocop-rake + +AllCops: + TargetRubyVersion: 2.7 + CacheRootDirectory: rubocop_cache + Exclude: + - 'tmp/**/*' + - 'bin/**/*' + - 'vendor/**/*' \ No newline at end of file diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..dd48eafb --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,273 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2024-06-26 13:45:40 UTC using RuboCop version 1.56.4. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 8 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Exclude: + - 'spec/has_many_spec.rb' + - 'spec/type_nested_spec.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowedMethods. +# AllowedMethods: instance_of?, kind_of?, is_a?, eql?, respond_to?, equal? +Lint/RedundantSafeNavigation: + Exclude: + - 'lib/couchbase-orm.rb' + +# Offense count: 4 +# Configuration parameters: IgnoreImplicitReferences. +Lint/ShadowedArgument: + Exclude: + - 'lib/couchbase-orm/relation.rb' + +# Offense count: 2 +# Configuration parameters: AllowComments, AllowNil. +Lint/SuppressedException: + Exclude: + - 'lib/couchbase-orm/views.rb' + - 'spec/relation_spec.rb' + +# Offense count: 5 +# Configuration parameters: AllowKeywordBlockArguments. +Lint/UnderscorePrefixedVariableName: + Exclude: + - 'lib/couchbase-orm/relation.rb' + +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/UselessAssignment: + Exclude: + - 'lib/couchbase-orm.rb' + - 'spec/base_spec.rb' + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. +# AllowedMethods: refine +Metrics/BlockLength: + Max: 27 + +# Offense count: 1 +# Configuration parameters: CountBlocks. +Metrics/BlockNesting: + Max: 4 + +# Offense count: 9 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 82 + +# Offense count: 3 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ModuleLength: + Max: 138 + +# Offense count: 3 +# Configuration parameters: CountKeywordArgs, MaxOptionalParameters. +Metrics/ParameterLists: + Max: 8 + +# Offense count: 1 +# Configuration parameters: AllowedNames. +# AllowedNames: module_parent +Naming/ClassAndModuleCamelCase: + Exclude: + - 'lib/couchbase-orm/relation.rb' + +# Offense count: 2 +Naming/ConstantName: + Exclude: + - 'lib/couchbase-orm/id_generator.rb' + - 'lib/couchbase-orm/views.rb' + +# Offense count: 1 +# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. +# CheckDefinitionPathHierarchyRoots: lib, spec, test, src +# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS +Naming/FileName: + Exclude: + - 'lib/couchbase-orm.rb' + +# Offense count: 3 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyleForLeadingUnderscores. +# SupportedStylesForLeadingUnderscores: disallowed, required, optional +Naming/MemoizedInstanceVariableName: + Exclude: + - 'lib/rails/generators/couchbase_orm/config/config_generator.rb' + - 'lib/rails/generators/couchbase_orm_generator.rb' + - 'spec/support.rb' + +# Offense count: 3 +# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros. +# NamePrefix: is_, has_, have_ +# ForbiddenPrefixes: is_, has_, have_ +# AllowedMethods: is_a? +# MethodDefinitionMacros: define_method, define_singleton_method +Naming/PredicateName: + Exclude: + - 'spec/**/*' + - 'lib/couchbase-orm/associations.rb' + - 'lib/couchbase-orm/base.rb' + - 'lib/couchbase-orm/utilities/has_many.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: SafeMultiline. +Performance/DeleteSuffix: + Exclude: + - 'lib/couchbase-orm/extensions/string.rb' + +# Offense count: 12 +# This cop supports unsafe autocorrection (--autocorrect-all). +RSpec/BeEq: + Exclude: + - 'spec/index_spec.rb' + - 'spec/relation_spec.rb' + - 'spec/type_spec.rb' + +# Offense count: 4 +RSpec/BeforeAfterAll: + Exclude: + - '**/spec/spec_helper.rb' + - '**/spec/rails_helper.rb' + - '**/spec/support/**/*.rb' + - 'spec/has_many_spec.rb' + - 'spec/n1ql_spec.rb' + - 'spec/relation_nested_spec.rb' + - 'spec/relation_spec.rb' + +# Offense count: 3 +# Configuration parameters: Prefixes, AllowedPatterns. +# Prefixes: when, with, without +RSpec/ContextWording: + Exclude: + - 'spec/attribute_dynamic_spec.rb' + - 'spec/base_spec.rb' + +# Offense count: 21 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. +# SupportedStyles: described_class, explicit +RSpec/DescribedClass: + Exclude: + - 'spec/collection_proxy_spec.rb' + - 'spec/id_generator_spec.rb' + - 'spec/type_nested_spec.rb' + - 'spec/type_spec.rb' + +# Offense count: 85 +# Configuration parameters: CountAsOne. +RSpec/ExampleLength: + Max: 39 + +# Offense count: 2 +RSpec/IdenticalEqualityAssertion: + Exclude: + - 'spec/base_spec.rb' + +# Offense count: 51 +# Configuration parameters: AssignmentOnly. +RSpec/InstanceVariable: + Exclude: + - 'spec/has_many_spec.rb' + +# Offense count: 8 +RSpec/LeakyConstantDeclaration: + Exclude: + - 'spec/has_many_spec.rb' + - 'spec/type_nested_spec.rb' + +# Offense count: 1 +# Configuration parameters: . +# SupportedStyles: have_received, receive +RSpec/MessageSpies: + EnforcedStyle: receive + +# Offense count: 1 +RSpec/MultipleDescribes: + Exclude: + - 'spec/type_spec.rb' + +# Offense count: 79 +RSpec/MultipleExpectations: + Max: 18 + +# Offense count: 1 +# Configuration parameters: AllowedPatterns. +# AllowedPatterns: ^expect_, ^assert_ +RSpec/NoExpectationExample: + Exclude: + - 'spec/support.rb' + +# Offense count: 1 +RSpec/PendingWithoutReason: + Exclude: + - 'spec/base_spec.rb' + +# Offense count: 4 +RSpec/RepeatedDescription: + Exclude: + - 'spec/type_spec.rb' + +# Offense count: 16 +# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. +# Include: **/*_spec.rb +RSpec/SpecFilePathFormat: + Enabled: false + +# Offense count: 6 +Style/ClassVars: + Exclude: + - 'lib/couchbase-orm.rb' + - 'lib/couchbase-orm/connection.rb' + - 'lib/couchbase-orm/utilities/ignored_properties.rb' + +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/GlobalStdStream: + Exclude: + - 'lib/couchbase-orm.rb' + +# Offense count: 8 +Style/MissingRespondToMissing: + Exclude: + - 'lib/couchbase-orm/attributes/dynamic.rb' + - 'lib/couchbase-orm/proxies/bucket_proxy.rb' + - 'lib/couchbase-orm/proxies/collection_proxy.rb' + - 'lib/couchbase-orm/proxies/n1ql_proxy.rb' + - 'lib/couchbase-orm/proxies/results_proxy.rb' + - 'lib/couchbase-orm/relation.rb' + +# Offense count: 4 +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: + Exclude: + - 'lib/couchbase-orm/attributes/dynamic.rb' + - 'lib/couchbase-orm/relation.rb' + - 'lib/couchbase-orm/utilities/join.rb' + +# Offense count: 4 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'lib/couchbase-orm/extensions/string.rb' + - 'lib/couchbase-orm/n1ql.rb' + - 'lib/couchbase-orm/proxies/n1ql_proxy.rb' + - 'lib/couchbase-orm/views.rb' + +# Offense count: 29 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 733 diff --git a/Gemfile b/Gemfile index 5e793866..65c3aa65 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,8 @@ +# frozen_string_literal: true + git_source(:github) do |repo_name| repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/') "https://github.com/#{repo_name}.git" end -source "https://rubygems.org" -gemspec \ No newline at end of file +source 'https://rubygems.org' +gemspec diff --git a/Rakefile b/Rakefile index aae699b0..52f0c170 100644 --- a/Rakefile +++ b/Rakefile @@ -1,15 +1,17 @@ +# frozen_string_literal: true + require 'rubygems' require 'rspec/core/rake_task' # testing framework require 'yard' # yard documentation # By default we don't run network tests -task :default => :test +task default: :test RSpec::Core::RakeTask.new(:spec) desc 'Run all tests' -task :test => [:spec] +task test: [:spec] YARD::Rake::YardocTask.new do |t| - t.files = ['lib/**/*.rb', '-', 'README.md'] + t.files = ['lib/**/*.rb', '-', 'README.md'] end diff --git a/couchbase-orm.gemspec b/couchbase-orm.gemspec index 5caaef16..d1bc71d4 100644 --- a/couchbase-orm.gemspec +++ b/couchbase-orm.gemspec @@ -1,34 +1,39 @@ -require File.expand_path("../lib/couchbase-orm/version", __FILE__) +# frozen_string_literal: true + +require File.expand_path('lib/couchbase-orm/version', __dir__) Gem::Specification.new do |gem| - gem.name = "couchbase-orm" - gem.version = CouchbaseOrm::VERSION - gem.license = 'MIT' - gem.authors = ["Stephen von Takach"] - gem.email = ["steve@cotag.me"] - gem.homepage = "https://github.com/cotag/couchbase-orm" - gem.summary = "Couchbase ORM for Rails" - gem.description = "A Couchbase ORM for Rails" + gem.name = 'couchbase-orm' + gem.version = CouchbaseOrm::VERSION + gem.license = 'MIT' + gem.authors = ['Stephen von Takach'] + gem.email = ['steve@cotag.me'] + gem.homepage = 'https://github.com/cotag/couchbase-orm' + gem.summary = 'Couchbase ORM for Rails' + gem.description = 'A Couchbase ORM for Rails' - gem.required_ruby_version = '>= 2.1.0' - gem.require_paths = ["lib"] + gem.required_ruby_version = '>= 2.7.0' + gem.require_paths = ['lib'] - gem.add_runtime_dependency 'activemodel', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.2', '< 7.1' - gem.add_runtime_dependency 'activerecord', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.2', '< 7.1' + gem.add_runtime_dependency 'activemodel', ENV['ACTIVE_MODEL_VERSION'] || '>= 5.2', '< 7.1' + gem.add_runtime_dependency 'activerecord', ENV['ACTIVE_MODEL_VERSION'] || '>= 5.2', '< 7.1' - gem.add_runtime_dependency 'couchbase', '~> 3.3.0' - gem.add_runtime_dependency 'radix', '~> 2.2' # converting numbers to and from any base + gem.add_runtime_dependency 'couchbase', '~> 3.3.0' + 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 'pry' - gem.add_development_dependency 'pry-stack_explorer' - gem.add_development_dependency 'simplecov' - gem.add_development_dependency 'actionpack', ENV["ACTIVE_MODEL_VERSION"] || '>= 5.2', '< 7.1' - gem.add_development_dependency 'timecop' - gem.add_development_dependency 'base64' + gem.add_development_dependency 'actionpack', ENV['ACTIVE_MODEL_VERSION'] || '>= 5.2', '< 7.1' + gem.add_development_dependency 'base64' + gem.add_development_dependency 'mapotempo_rubocop', '<1.0' + gem.add_development_dependency 'pry' + gem.add_development_dependency 'pry-stack_explorer' + gem.add_development_dependency 'rake', '~> 12.2' + gem.add_development_dependency 'rspec', '~> 3.7' + gem.add_development_dependency 'rubocop-rake', '<1.0' + gem.add_development_dependency 'rubocop-rspec', '~>3.0' + gem.add_development_dependency 'simplecov' + gem.add_development_dependency 'timecop' + gem.add_development_dependency 'yard', '~> 0.9' - gem.files = `git ls-files`.split("\n") - gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + gem.files = `git ls-files`.split("\n") + gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") end diff --git a/lib/couchbase-orm.rb b/lib/couchbase-orm.rb index 849a9e8d..f4924679 100644 --- a/lib/couchbase-orm.rb +++ b/lib/couchbase-orm.rb @@ -1,93 +1,96 @@ # frozen_string_literal: true, encoding: ASCII-8BIT -require "logger" -require "active_support/lazy_load_hooks" +# frozen_string_literal: true + +require 'logger' +require 'active_support/lazy_load_hooks' ActiveSupport.on_load(:i18n) do - I18n.load_path << File.expand_path("couchbase-orm/locale/en.yml", __dir__) + I18n.load_path << File.expand_path('couchbase-orm/locale/en.yml', __dir__) end module CouchbaseOrm - autoload :Encrypt, 'couchbase-orm/encrypt' - autoload :Error, 'couchbase-orm/error' - autoload :Connection, 'couchbase-orm/connection' - autoload :IdGenerator, 'couchbase-orm/id_generator' - autoload :Base, 'couchbase-orm/base' - autoload :Document, 'couchbase-orm/base' - autoload :NestedDocument, 'couchbase-orm/base' - autoload :HasMany, 'couchbase-orm/utilities/has_many' - autoload :AttributesDynamic, 'couchbase-orm/attributes/dynamic' - - def self.logger - @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO unless ENV["COUCHBASE_ORM_DEBUG"] } - end - - def self.logger=(logger) - @@logger = logger + autoload :Encrypt, 'couchbase-orm/encrypt' + autoload :Error, 'couchbase-orm/error' + autoload :Connection, 'couchbase-orm/connection' + autoload :IdGenerator, 'couchbase-orm/id_generator' + autoload :Base, 'couchbase-orm/base' + autoload :Document, 'couchbase-orm/base' + autoload :NestedDocument, 'couchbase-orm/base' + autoload :HasMany, 'couchbase-orm/utilities/has_many' + autoload :AttributesDynamic, 'couchbase-orm/attributes/dynamic' + + def self.logger + @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| + l.level = Logger::INFO unless ENV['COUCHBASE_ORM_DEBUG'] + } + end + + def self.logger=(logger) + @@logger = logger + end + + def self.try_load(id) + result = nil + was_array = id.is_a?(Array) + query_id = if was_array && id.length == 1 + id.first + else + id + 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 - def self.try_load(id) - result = nil - was_array = id.is_a?(Array) - if was_array && id.length == 1 - query_id = id.first - else - query_id = id - 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 + try_load_create_model(result, id) + end - if result&.is_a?(Array) - return result.zip(id).map { |r, id| try_load_create_model(r, id) }.compact - end + def self.try_load_create_model(result, id) + ddoc = result&.content&.[]('type') + return nil unless ddoc - return try_load_create_model(result, id) - end - - 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 + ::CouchbaseOrm::Base.descendants.each do |model| + if model.design_document == ddoc + return model.new(result, id: id) + end end + nil + end end # Provide Boolean conversion function # See: http://www.virtuouscode.com/2012/05/07/a-ruby-conversion-idiom/ module Kernel - private - - def Boolean(value) - case value - when String, Symbol - case value.to_s.strip.downcase - when 'true' - return true - when 'false' - return false - end - when Integer - return value != 0 - when false, nil - return false - when true - return true - end - - raise ArgumentError, "invalid value for Boolean(): \"#{value.inspect}\"" + private + + def Boolean(value) # rubocop:disable Naming/MethodName + case value + when String, Symbol + case value.to_s.strip.downcase + when 'true' + return true + when 'false' + return false + end + when Integer + return value != 0 + when false, nil + return false + when true + return true end + + raise ArgumentError.new("invalid value for Boolean(): \"#{value.inspect}\"") + end end + class Boolean < TrueClass; end # If we are using Rails then we will include the Couchbase railtie. if defined?(Rails) - require 'couchbase-orm/railtie' + require 'couchbase-orm/railtie' end - diff --git a/lib/couchbase-orm/associations.rb b/lib/couchbase-orm/associations.rb index c15ac66b..96ef6eee 100644 --- a/lib/couchbase-orm/associations.rb +++ b/lib/couchbase-orm/associations.rb @@ -1,218 +1,224 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true require 'active_model' module CouchbaseOrm - module Associations - extend ActiveSupport::Concern - - - module ClassMethods - # Defines a belongs_to association for the model - def belongs_to(name, **options) - @associations ||= [] - @associations << [name.to_sym, options[:dependent]] - - ref = options[:foreign_key] || :"#{name}_id" - ref_ass = :"#{ref}=" - instance_var = :"@__assoc_#{name}" - - # Class reference - assoc = (options[:class_name] || name.to_s.camelize).to_s - - # Create the local setter / getter - attribute(ref) { |value| - remove_instance_variable(instance_var) if instance_variable_defined?(instance_var) - value - } - - # Define reader - define_method(name) do - return instance_variable_get(instance_var) if instance_variable_defined?(instance_var) - val = if options[:polymorphic] - ::CouchbaseOrm.try_load(self.send(ref)) - else - assoc.constantize.find(self.send(ref), quiet: true) - end - instance_variable_set(instance_var, val) - val + module Associations + extend ActiveSupport::Concern + + module ClassMethods + # Defines a belongs_to association for the model + def belongs_to(name, **options) + @associations ||= [] + @associations << [name.to_sym, options[:dependent]] + + ref = options[:foreign_key] || :"#{name}_id" + ref_ass = :"#{ref}=" + instance_var = :"@__assoc_#{name}" + + # Class reference + assoc = (options[:class_name] || name.to_s.camelize).to_s + + # Create the local setter / getter + attribute(ref) { |value| + remove_instance_variable(instance_var) if instance_variable_defined?(instance_var) + value + } + + # Define reader + define_method(name) do + return instance_variable_get(instance_var) if instance_variable_defined?(instance_var) + + val = if options[:polymorphic] + ::CouchbaseOrm.try_load(self.send(ref)) + else + assoc.constantize.find(self.send(ref), quiet: true) end + instance_variable_set(instance_var, val) + val + end - # Define writer - attr_writer name - define_method(:"#{name}=") do |value| - if value - if !options[:polymorphic] - klass = assoc.constantize - raise ArgumentError, "type mismatch on association: #{klass.design_document} != #{value.class.design_document}" if klass.design_document != value.class.design_document - end - self.send(ref_ass, value.id) - else - self.send(ref_ass, nil) - end - - instance_variable_set(instance_var, value) - end + # Define writer + attr_writer name - define_method(:"#{name}_reset") do - remove_instance_variable(instance_var) if instance_variable_defined?(instance_var) - end + define_method(:"#{name}=") do |value| + if value + if !options[:polymorphic] + klass = assoc.constantize + raise ArgumentError.new("type mismatch on association: #{klass.design_document} != #{value.class.design_document}") if klass.design_document != value.class.design_document end + self.send(ref_ass, value.id) + else + self.send(ref_ass, nil) + end - def has_and_belongs_to_many(name, **options) - @associations ||= [] - @associations << [name.to_sym, options[:dependent]] - - ref = options[:foreign_key] || :"#{name.to_s.singularize}_ids" - ref_ass = :"#{ref}=" - instance_var = :"@__assoc_#{name}" - - # Class reference - assoc = (options[:class_name] || name.to_s.singularize.camelize).to_s - - # Create the local setter / getter - attribute(ref) { |value| - remove_instance_variable(instance_var) if instance_variable_defined?(instance_var) - value - } - - # 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(ref_value) if ref_value - else - assoc.constantize.find(ref_value) if ref_value - end - val = Array.wrap(val || []) - instance_variable_set(instance_var, val) - val - end + instance_variable_set(instance_var, value) + end - # Define writer - attr_writer name - define_method(:"#{name}=") do |value| - if value - if !options[:polymorphic] - klass = assoc.constantize - value.each do |v| - raise ArgumentError, "type mismatch on association: #{klass.design_document} != #{v.class.design_document}" if klass.design_document != v.class.design_document - end - end - self.send(ref_ass, value.map(&:id)) - else - self.send(ref_ass, nil) - end - - instance_variable_set(instance_var, value) + define_method(:"#{name}_reset") do + remove_instance_variable(instance_var) if instance_variable_defined?(instance_var) + end + end + + def has_and_belongs_to_many(name, **options) + @associations ||= [] + @associations << [name.to_sym, options[:dependent]] + + ref = options[:foreign_key] || :"#{name.to_s.singularize}_ids" + ref_ass = :"#{ref}=" + instance_var = :"@__assoc_#{name}" + + # Class reference + assoc = (options[:class_name] || name.to_s.singularize.camelize).to_s + + # Create the local setter / getter + attribute(ref) { |value| + remove_instance_variable(instance_var) if instance_variable_defined?(instance_var) + value + } + + # 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 ref_value.nil? + nil + elsif options[:polymorphic] + ::CouchbaseOrm.try_load(ref_value) + else + assoc.constantize.find(ref_value) end + val = Array.wrap(val || []) + instance_variable_set(instance_var, val) + val + end - define_method(:"#{name}_reset") do - self.remove_instance_variable(instance_var) if self.instance_variable_defined?(instance_var) - end + # Define writer + attr_writer name - return unless options[:autosave] + define_method(:"#{name}=") do |value| + if value + if !options[:polymorphic] + klass = assoc.constantize + value.each do |v| + raise ArgumentError.new("type mismatch on association: #{klass.design_document} != #{v.class.design_document}") if klass.design_document != v.class.design_document + end + end + self.send(ref_ass, value.map(&:id)) + else + self.send(ref_ass, nil) + end - save_method = :"autosave_associated_records_for_#{name}" + instance_variable_set(instance_var, value) + end - define_non_cyclic_method(save_method) do - old, new = previous_changes[ref] - adds = (new || []) - (old || []) - subs = (old || []) - (new || []) - 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 + define_method(:"#{name}_reset") do + self.remove_instance_variable(instance_var) if self.instance_variable_defined?(instance_var) + end - after_create save_method - after_update save_method - end + return unless options[:autosave] - def associations - @associations || [] - end + save_method = :"autosave_associated_records_for_#{name}" - def define_non_cyclic_method(name, &block) - return if method_defined?(name) - - define_method(name) do |*args| - result = true; @_already_called ||= {} - # Loop prevention for validation of associations - unless @_already_called[name] - begin - @_already_called[name] = true - result = instance_eval(&block) - ensure - @_already_called[name] = false - end - end - result - end - end + define_non_cyclic_method(save_method) do + old, new = previous_changes[ref] + adds = (new || []) - (old || []) + subs = (old || []) - (new || []) + 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 - def update_has_and_belongs_to_many_reverse_association(assoc, keys, is_add, **options) - remote_method = options[:inverse_of] || self.class.to_s.pluralize.underscore.to_sym - return if keys.empty? - - models = if options[:polymorphic] - ::CouchbaseOrm.try_load(keys) - else - assoc.constantize.find(keys, quiet: true) - end - models = Array.wrap(models) - models.each do |v| - next unless v.respond_to?(remote_method) - - tab = v.__send__(remote_method) || [] - index = tab.find_index(self) - if is_add && !index - tab = tab.dup - tab.push(self) - elsif !is_add && index - tab = tab.dup - tab.delete_at(index) - else - next - end - v[remote_method] = tab - v.save! + after_create save_method + after_update save_method + end + + def associations + @associations || [] + end + + def define_non_cyclic_method(name, &block) + return if method_defined?(name) + + define_method(name) do |*_args| + result = true + @_already_called ||= {} + # Loop prevention for validation of associations + unless @_already_called[name] + begin + @_already_called[name] = true + result = instance_eval(&block) + ensure + @_already_called[name] = false end + end + result end + end + end - def destroy_associations! - assoc = self.class.associations - assoc.each do |name, dependent| - next unless dependent - - model = self.__send__(name) - if model.present? - case dependent - when :destroy, :delete - if model.respond_to?(:stream) - model.stream { |mod| mod.__send__(dependent) } - elsif model.is_a?(Array) || model.is_a?(CouchbaseOrm::ResultsProxy) - model.each { |m| m.__send__(dependent) } - else - model.__send__(dependent) - end - when :restrict_with_exception - raise RecordExists.new("#{self.class.name} instance maintains a restricted reference to #{name}", self) - when :restrict_with_error - # TODO:: - end - end - end + def update_has_and_belongs_to_many_reverse_association(assoc, keys, is_add, **options) + remote_method = options[:inverse_of] || self.class.to_s.pluralize.underscore.to_sym + return if keys.empty? + + models = if options[:polymorphic] + ::CouchbaseOrm.try_load(keys) + else + assoc.constantize.find(keys, quiet: true) + end + models = Array.wrap(models) + models.each do |v| + next unless v.respond_to?(remote_method) + + tab = v.__send__(remote_method) || [] + index = tab.find_index(self) + if is_add && !index + tab = tab.dup + tab.push(self) + elsif !is_add && index + tab = tab.dup + tab.delete_at(index) + else + next end + v[remote_method] = tab + v.save! + end + end - def reset_associations - assoc = self.class.associations - assoc.each do |name, _| - instance_var = :"@__assoc_#{name}" - remove_instance_variable(instance_var) if instance_variable_defined?(instance_var) - end + def destroy_associations! + assoc = self.class.associations + assoc.each do |name, dependent| + next unless dependent + + model = self.__send__(name) + next unless model.present? + + case dependent + when :destroy, :delete + if model.respond_to?(:stream) + model.stream { |mod| mod.__send__(dependent) } + elsif model.is_a?(Array) || model.is_a?(CouchbaseOrm::ResultsProxy) + model.each { |m| m.__send__(dependent) } + else + model.__send__(dependent) + end + when :restrict_with_exception + raise RecordExists.new("#{self.class.name} instance maintains a restricted reference to #{name}", self) + # when :restrict_with_error + # TODO: : end + end + end + + def reset_associations + assoc = self.class.associations + assoc.each do |name, _| + instance_var = :"@__assoc_#{name}" + remove_instance_variable(instance_var) if instance_variable_defined?(instance_var) + end end + end end diff --git a/lib/couchbase-orm/attributes/dynamic.rb b/lib/couchbase-orm/attributes/dynamic.rb index ca0a1711..35863bfa 100644 --- a/lib/couchbase-orm/attributes/dynamic.rb +++ b/lib/couchbase-orm/attributes/dynamic.rb @@ -1,99 +1,100 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true module CouchbaseOrm - module AttributesDynamic - extend ActiveSupport::Concern + module AttributesDynamic + extend ActiveSupport::Concern - # Override respond_to? so it responds properly for dynamic attributes. - # - # @example Does this object respond to the method? - # person.respond_to?(:title) - # - # @param [ Array ] name The name of the method. - # @param [ true | false ] include_private - # - # @return [ true | false ] True if it does, false if not. - def respond_to?(name, include_private = false) - super || attributes&.key?(name.to_s.reader) - end + # Override respond_to? so it responds properly for dynamic attributes. + # + # @example Does this object respond to the method? + # person.respond_to?(:title) + # + # @param [ Array ] name The name of the method. + # @param [ true | false ] include_private + # + # @return [ true | false ] True if it does, false if not. + def respond_to?(name, include_private = false) + super || attributes&.key?(name.to_s.reader) + end - private + private - # Override private _assign_attribute to accept dynamic attribute - # - # @param [ String ] name of attribute - # @param [ Object ] value of attribute - # - # @return [ Object ] value of attribute - def _assign_attribute(name, value) - responds = name.reader == 'id' || respond_to?(name.writer) - if responds - public_send(name.writer, value) - else - type = value.class.to_s.underscore.to_sym - type = :hash if type == :"active_support/hash_with_indifferent_access" - type = ActiveModel::Type.lookup(type) - @attributes[name] = ActiveModel::Attribute.from_database(name, value, type) - end - end + # Override private _assign_attribute to accept dynamic attribute + # + # @param [ String ] name of attribute + # @param [ Object ] value of attribute + # + # @return [ Object ] value of attribute + def _assign_attribute(name, value) + responds = name.reader == 'id' || respond_to?(name.writer) + if responds + public_send(name.writer, value) + else + type = value.class.to_s.underscore.to_sym + type = :hash if type == :"active_support/hash_with_indifferent_access" + type = ActiveModel::Type.lookup(type) + @attributes[name] = ActiveModel::Attribute.from_database(name, value, type) + end + end - # Define a reader method for a dynamic attribute. - # - # @example Define a reader method. - # model.define_dynamic_reader(:field) - # - # @param [ String ] name The name of the field. - def define_dynamic_reader(name) - return unless name.valid_method_name? + # Define a reader method for a dynamic attribute. + # + # @example Define a reader method. + # model.define_dynamic_reader(:field) + # + # @param [ String ] name The name of the field. + def define_dynamic_reader(name) + return unless name.valid_method_name? - instance_eval do - define_singleton_method(name) do - @attributes[getter].value - end - end + instance_eval do + define_singleton_method(name) do + @attributes[getter].value end + end + end - # Define a writer method for a dynamic attribute. - # - # @example Define a writer method. - # model.define_dynamic_writer(:field) - # - # @param [ String ] name The name of the field. - def define_dynamic_writer(name) - return unless name.valid_method_name? + # Define a writer method for a dynamic attribute. + # + # @example Define a writer method. + # model.define_dynamic_writer(:field) + # + # @param [ String ] name The name of the field. + def define_dynamic_writer(name) + return unless name.valid_method_name? - instance_eval do - define_singleton_method("#{name}=") do |value| - @attributes.write_from_user(name, value) - value - end - end + instance_eval do + define_singleton_method("#{name}=") do |value| + @attributes.write_from_user(name, value) + value end + end + end - # Used for allowing accessor methods for dynamic attributes. - # - # @api private - # - # @example Call through method_missing. - # document.method_missing(:test) - # - # @param [ String | Symbol ] name The name of the method. - # @param [ Object... ] *args The arguments to the method. - # - # @return [ Object ] The result of the method call. - def method_missing(name, *args) - attr = name.to_s - return super unless attr.reader != 'id' && attributes.key?(attr.reader) + # Used for allowing accessor methods for dynamic attributes. + # + # @api private + # + # @example Call through method_missing. + # document.method_missing(:test) + # + # @param [ String | Symbol ] name The name of the method. + # @param [ Object... ] *args The arguments to the method. + # + # @return [ Object ] The result of the method call. + def method_missing(name, *args) + attr = name.to_s + return super unless attr.reader != 'id' && attributes.key?(attr.reader) - getter = attr.reader - if attr.writer? - define_dynamic_writer(getter) - @attributes.write_from_user(getter, args.first) - args.first - else - define_dynamic_reader(getter) - @attributes[getter].value - end - end + getter = attr.reader + if attr.writer? + define_dynamic_writer(getter) + @attributes.write_from_user(getter, args.first) + args.first + else + define_dynamic_reader(getter) + @attributes[getter].value + end end -end \ No newline at end of file + end +end diff --git a/lib/couchbase-orm/base.rb b/lib/couchbase-orm/base.rb index 8dece9b7..317c1a63 100644 --- a/lib/couchbase-orm/base.rb +++ b/lib/couchbase-orm/base.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true, encoding: ASCII-8BIT - +# frozen_string_literal: true require 'active_model' require 'active_record' if ActiveModel::VERSION::MAJOR >= 6 - require 'active_record/database_configurations' + require 'active_record/database_configurations' else - require 'active_model/type' + require 'active_model/type' end require 'active_support/hash_with_indifferent_access' require 'couchbase' @@ -29,321 +29,323 @@ require 'couchbase-orm/utilities/ignored_properties' require 'couchbase-orm/json_transcoder' - 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 - 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 - - def abstract_class? - false - end - - def connected? - true - end - - def table_exists? - true - end - - def _reflect_on_association(attribute) - false - end - - def type_for_attribute(attribute) - attribute_types[attribute] - end - - if ActiveModel::VERSION::MAJOR < 6 - def attribute_names - attribute_types.keys - end - end - end + extend ActiveSupport::Concern - def _has_attribute?(attr_name) - attribute_names.include?(attr_name.to_s) - end + module ClassMethods + def primary_key + 'id' + end - def attribute_for_inspect(attr_name) - value = send(attr_name) - value.inspect - end + def base_class? + true + end + + # can't be an alias for now + def column_names + attribute_names + end - if ActiveModel::VERSION::MAJOR <= 6 - def format_for_inspect(value) - if value.is_a?(String) && value.length > 50 - "#{value[0, 50]}...".inspect - elsif value.is_a?(Date) || value.is_a?(Time) - %("#{value.to_s(:db)}") - else - value.inspect - end - end - - 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 - - def read_attribute(attr_name, &block) - name = attr_name.to_s - name = self.class.attribute_aliases[name] || name - - name = @primary_key if name == "id" && @primary_key - @attributes.fetch_value(name, &block) - end + def abstract_class? + false + end + + def connected? + true + end + + def table_exists? + true + end + + def _reflect_on_association(_attribute) + false + end + + def type_for_attribute(attribute) + attribute_types[attribute] + end + + if ActiveModel::VERSION::MAJOR < 6 + def attribute_names + attribute_types.keys end + end + end + + def _has_attribute?(attr_name) + attribute_names.include?(attr_name.to_s) end - class Document - include ::ActiveModel::Model - include ::ActiveModel::Dirty - include ::ActiveModel::Attributes - include ::ActiveModel::Serializers::JSON + def attribute_for_inspect(attr_name) + value = send(attr_name) + value.inspect + end - include ::ActiveModel::Validations - include ::ActiveModel::Validations::Callbacks + if ActiveModel::VERSION::MAJOR <= 6 + def format_for_inspect(value) + if value.is_a?(String) && value.length > 50 + "#{value[0, 50]}...".inspect + elsif value.is_a?(Date) || value.is_a?(Time) + %("#{value.to_s(:db)}") + else + value.inspect + end + end - include ::ActiveRecord::Core - include ActiveRecordCompat - include Encrypt + def attribute_names + self.class.attribute_names + end - extend Enum + def has_attribute?(attr_name) + @attributes.key?(attr_name.to_s) + end - define_model_callbacks :initialize, :only => :after - define_model_callbacks :create, :destroy, :save, :update + def attribute_present?(attribute) + value = send(attribute) + !value.nil? && !(value.respond_to?(:empty?) && value.empty?) + end - Metadata = Struct.new(:cas) + def _write_attribute(attr_name, value) + @attributes.write_from_user(attr_name.to_s, value) + value + end - class MismatchTypeError < RuntimeError; end + def read_attribute(attr_name, &block) + name = attr_name.to_s + name = self.class.attribute_aliases[name] || name - def initialize(model = nil, ignore_doc_type: false, **attributes) - CouchbaseOrm.logger.debug { "Initialize model #{model} with #{attributes.to_s.truncate(200)}" } - @__metadata__ = Metadata.new + name = @primary_key if name == 'id' && @primary_key + @attributes.fetch_value(name, &block) + end + end + end - super() + class Document + include ::ActiveModel::Model + include ::ActiveModel::Dirty + include ::ActiveModel::Attributes + include ::ActiveModel::Serializers::JSON - if model - case model - when Couchbase::Collection::GetResult - doc = HashWithIndifferentAccess.new(model.content) || raise('empty response provided') - type = doc.delete(:type) - doc.delete(:id) + include ::ActiveModel::Validations + include ::ActiveModel::Validations::Callbacks - 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 + include ::ActiveRecord::Core + include ActiveRecordCompat + include Encrypt - self.id = attributes[:id] if attributes[:id].present? - @__metadata__.cas = model.cas + extend Enum - assign_attributes(decode_encrypted_attributes(doc)) - clear_changes_information - when CouchbaseOrm::Base - clear_changes_information - super(model.attributes.except(:id, 'type')) - else - clear_changes_information - super(decode_encrypted_attributes(**attributes.merge(Hash(model)).symbolize_keys)) - end - else - clear_changes_information - super(attributes) - end + define_model_callbacks :initialize, only: :after + define_model_callbacks :create, :destroy, :save, :update - yield self if block_given? + Metadata = Struct.new(:cas) - run_callbacks :initialize - end + class MismatchTypeError < RuntimeError; end - def attributes - super.with_indifferent_access - end + def initialize(model = nil, ignore_doc_type: false, **attributes) + CouchbaseOrm.logger.debug { "Initialize model #{model} with #{attributes.to_s.truncate(200)}" } + @__metadata__ = Metadata.new - def [](key) - send(key) - end + super() - def []=(key, value) - send(:"#{key}=", value) - end + if model + case model + when Couchbase::Collection::GetResult + 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 - protected + self.id = attributes[:id] if attributes[:id].present? + @__metadata__.cas = model.cas - def serialized_attributes - encode_encrypted_attributes.map { |k, v| - [k, self.class.attribute_types[k].serialize(v)] - }.to_h + assign_attributes(decode_encrypted_attributes(doc)) + clear_changes_information + when CouchbaseOrm::Base + clear_changes_information + super(model.attributes.except(:id, 'type')) + else + clear_changes_information + super(decode_encrypted_attributes(**attributes.merge(Hash(model)).symbolize_keys)) end + else + clear_changes_information + super(attributes) + end + + yield self if block_given? + + run_callbacks :initialize end - class NestedDocument < Document - def initialize(*args, **kwargs) - super - if respond_to?(:id) && id.nil? - assign_attributes(id: SecureRandom.hex) - end - end + def attributes + super.with_indifferent_access end - class Base < Document - include Persistence - include ::ActiveRecord::AttributeMethods::Dirty - include ::ActiveRecord::Validations # must be included after Persistence - include ::ActiveRecord::Timestamp # must be included after Persistence - - include Associations - include Views - include QueryHelper - include N1ql - include Relation - - extend Join - extend Enum - extend EnsureUnique - extend HasMany - extend Index - extend IgnoredProperties - - class << self - def connect(**options) - @bucket = BucketProxy.new(::MTLibcouchbase::Bucket.new(**options)) - end - - def bucket=(bucket) - @bucket = bucket.is_a?(BucketProxy) ? bucket : BucketProxy.new(bucket) - end - - 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 - - def uuid_generator=(generator) - @uuid_generator = generator - end - - def find(*ids, quiet: false) - CouchbaseOrm.logger.debug { "Base.find(l##{ids.length}) #{ids}" } - - ids = ids.flatten.select { |id| id.present? } - if ids.empty? - raise CouchbaseOrm::Error::EmptyNotAllowed, 'no id(s) provided' unless quiet - return nil if quiet - end - - transcoder = CouchbaseOrm::JsonTranscoder.new(ignored_properties: ignored_properties) - records = quiet ? collection.get_multi(ids, transcoder: transcoder) : collection.get_multi!(ids, transcoder: transcoder) - return nil if records.nil? - - CouchbaseOrm.logger.debug { "Base.find found(#{records})" } - records = records.zip(ids).map { |record, id| - self.new(record, id: id) if record - } - records.compact! - ids.length > 1 ? records : records[0] - end - - def find_by_id(*ids, **options) - options[:quiet] = true - find(*ids, **options) - end - alias_method :[], :find_by_id - - def exists?(id) - CouchbaseOrm.logger.debug { "Data - Exists? #{id}" } - collection.exists(id).exists - end - alias_method :has_key?, :exists? - end + def [](key) + send(key) + end - def id=(value) - raise RuntimeError, 'ID cannot be changed' if @__metadata__.cas && value - attribute_will_change!(:id) - _write_attribute("id", value) - end + def []=(key, value) + send(:"#{key}=", value) + end - # Public: Allows for access to ActiveModel functionality. - # - # Returns self. - def to_model - self - end + protected - # Public: Hashes identifying properties of the instance - # - # Ruby normally hashes an object to be used in comparisons. In our case - # we may have two techincally different objects referencing the same entity id. - # - # Returns a string representing the unique key. - def hash - "#{self.class.name}-#{self.id}-#{@__metadata__.cas}-#{@__attributes__.hash}".hash - end + def serialized_attributes + encode_encrypted_attributes.map { |k, v| + [k, self.class.attribute_types[k].serialize(v)] + }.to_h + end + end - # Public: Overrides eql? to use == in the comparison. - # - # other - Another object to compare to - # - # Returns a boolean. - def eql?(other) - self == other - end + class NestedDocument < Document + def initialize(*args, **kwargs) + super + return unless respond_to?(:id) && id.nil? - # Public: Overrides == to compare via class and entity id. - # - # other - Another object to compare to - # - # Returns a boolean. - def ==(other) - super || other.instance_of?(self.class) && !id.nil? && other.id == id + assign_attributes(id: SecureRandom.hex) + end + end + + class Base < Document + include Persistence + include ::ActiveRecord::AttributeMethods::Dirty + include ::ActiveRecord::Validations # must be included after Persistence + include ::ActiveRecord::Timestamp # must be included after Persistence + + include Associations + include Views + include QueryHelper + include N1ql + include Relation + + extend Join + extend Enum + extend EnsureUnique + extend HasMany + extend Index + extend IgnoredProperties + + class << self + def connect(**options) + @bucket = BucketProxy.new(::MTLibcouchbase::Bucket.new(**options)) + end + + def bucket=(bucket) + @bucket = bucket.is_a?(BucketProxy) ? bucket : BucketProxy.new(bucket) + end + + 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 + + attr_writer :uuid_generator + + def find(*ids, quiet: false) + CouchbaseOrm.logger.debug { "Base.find(l##{ids.length}) #{ids}" } + + ids = ids.flatten.select(&:present?) + if ids.empty? + raise CouchbaseOrm::Error::EmptyNotAllowed.new('no id(s) provided') unless quiet + return nil if quiet end - private + transcoder = CouchbaseOrm::JsonTranscoder.new(ignored_properties: ignored_properties) + records = quiet ? collection.get_multi(ids, + transcoder: transcoder) : collection.get_multi!(ids, + transcoder: transcoder) + return nil if records.nil? + + CouchbaseOrm.logger.debug { "Base.find found(#{records})" } + records = records.zip(ids).map { |record, id| + self.new(record, id: id) if record + } + records.compact! + ids.length > 1 ? records : records[0] + end + + def find_by_id(*ids, **options) + options[:quiet] = true + find(*ids, **options) + end + alias [] find_by_id + + def exists?(id) + CouchbaseOrm.logger.debug { "Data - Exists? #{id}" } + collection.exists(id).exists + end + alias has_key? exists? + end - def raise_validation_error - raise CouchbaseOrm::Error::RecordInvalid.new(self) - end + def id=(value) + raise 'ID cannot be changed' if @__metadata__.cas && value + + attribute_will_change!(:id) + _write_attribute('id', value) + end + + # Public: Allows for access to ActiveModel functionality. + # + # Returns self. + def to_model + self + end + + # Public: Hashes identifying properties of the instance + # + # Ruby normally hashes an object to be used in comparisons. In our case + # we may have two techincally different objects referencing the same entity id. + # + # Returns a string representing the unique key. + def hash + "#{self.class.name}-#{self.id}-#{@__metadata__.cas}-#{@__attributes__.hash}".hash + end + + # Public: Overrides eql? to use == in the comparison. + # + # other - Another object to compare to + # + # Returns a boolean. + def eql?(other) + self == other + end + + # Public: Overrides == to compare via class and entity id. + # + # other - Another object to compare to + # + # Returns a boolean. + def ==(other) + super || other.instance_of?(self.class) && !id.nil? && other.id == id + end + + private + + def raise_validation_error + raise CouchbaseOrm::Error::RecordInvalid.new(self) end + end end diff --git a/lib/couchbase-orm/connection.rb b/lib/couchbase-orm/connection.rb index 4c0dbb03..1f6dd05b 100644 --- a/lib/couchbase-orm/connection.rb +++ b/lib/couchbase-orm/connection.rb @@ -1,36 +1,40 @@ +# frozen_string_literal: true + require 'couchbase' module CouchbaseOrm - class Connection - @@config = nil - def self.config - @@config || { - :connection_string => ENV['COUCHBASE_HOST'] || '127.0.0.1', - :username => ENV['COUCHBASE_USER'], - :password => ENV['COUCHBASE_PASSWORD'], - :bucket => ENV['COUCHBASE_BUCKET'] - } - end + class Connection + @@config = nil + def self.config + @@config || { + connection_string: 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.deep_symbolize_keys - end + def self.config=(config) + @@config = config.deep_symbolize_keys + end - def self.cluster - @cluster ||= begin - cb_config = Couchbase::Configuration.new - cb_config.connection_string = config[:connection_string].presence.try { |s| "couchbase://#{s}" } || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm host') - cb_config.username = config[:username].presence || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm username') - cb_config.password = config[:password].presence || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm password') - Couchbase::Cluster.connect(cb_config) - end - end + def self.cluster + @cluster ||= begin + cb_config = Couchbase::Configuration.new + cb_config.connection_string = config[:connection_string].presence.try { |s| + "couchbase://#{s}" + } || raise(CouchbaseOrm::Error.new('Missing CouchbaseOrm host')) + cb_config.username = config[:username].presence || raise(CouchbaseOrm::Error.new('Missing CouchbaseOrm username')) + cb_config.password = config[:password].presence || raise(CouchbaseOrm::Error.new('Missing CouchbaseOrm password')) + Couchbase::Cluster.connect(cb_config) + end + end - def self.bucket - @bucket ||= begin - bucket_name = config[:bucket].presence || raise(CouchbaseOrm::Error, 'Missing CouchbaseOrm bucket name') - cluster.bucket(bucket_name) - end - end + def self.bucket + @bucket ||= begin + bucket_name = config[:bucket].presence || raise(CouchbaseOrm::Error.new('Missing CouchbaseOrm bucket name')) + cluster.bucket(bucket_name) + end end -end \ No newline at end of file + end +end diff --git a/lib/couchbase-orm/encrypt.rb b/lib/couchbase-orm/encrypt.rb index ee1c890c..26377003 100644 --- a/lib/couchbase-orm/encrypt.rb +++ b/lib/couchbase-orm/encrypt.rb @@ -1,48 +1,49 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true module CouchbaseOrm - module Encrypt - def encode_encrypted_attributes - attributes.map do |key, value| - type = self.class.attribute_types[key.to_s] - if type.is_a?(CouchbaseOrm::Types::Encrypted) - next unless value - raise "Can not serialize value #{value} of type '#{value.class}' for Tanker encrypted attribute" unless value.is_a?(String) - ["encrypted$#{key}", { - alg: type.alg, - ciphertext: value - }] - else - [key,value] - end - end.compact.to_h - end + module Encrypt + def encode_encrypted_attributes + attributes.map do |key, value| + type = self.class.attribute_types[key.to_s] + if type.is_a?(CouchbaseOrm::Types::Encrypted) + next unless value + raise "Can not serialize value #{value} of type '#{value.class}' for Tanker encrypted attribute" unless value.is_a?(String) - def decode_encrypted_attributes(attributes) - attributes.map do |key, value| - key = key.to_s - if key.start_with?('encrypted$') - key = key.gsub('encrypted$', '') - value = value.with_indifferent_access[:ciphertext] - end - [key, value] - end.to_h + ["encrypted$#{key}", { + alg: type.alg, + ciphertext: value + }] + else + [key, value] end + end.compact.to_h + end - - def to_json(*args, **kwargs) - as_json.to_json(*args, **kwargs) + def decode_encrypted_attributes(attributes) + attributes.map do |key, value| + key = key.to_s + if key.start_with?('encrypted$') + key = key.gsub('encrypted$', '') + value = value.with_indifferent_access[:ciphertext] end + [key, value] + end.to_h + end + + def to_json(*args, **kwargs) + as_json.to_json(*args, **kwargs) + end - def as_json(*args, **kwargs) - super(*args, **kwargs).map do |key, value| - type = self.class.attribute_types[key.to_s] - if type.is_a?(CouchbaseOrm::Types::Encrypted) && value - raise "Can not serialize value #{value} of type '#{value.class}' for encrypted attribute" unless value.is_a?(String) - end - [key, value] - end.to_h.with_indifferent_access + def as_json(*args, **kwargs) + super(*args, **kwargs).map do |key, value| + type = self.class.attribute_types[key.to_s] + if type.is_a?(CouchbaseOrm::Types::Encrypted) && value && !value.is_a?(String) + raise "Can not serialize value #{value} of type '#{value.class}' for encrypted attribute" end + [key, value] + end.to_h.with_indifferent_access end + end end diff --git a/lib/couchbase-orm/error.rb b/lib/couchbase-orm/error.rb index 4f0f7fd1..015c28ec 100644 --- a/lib/couchbase-orm/error.rb +++ b/lib/couchbase-orm/error.rb @@ -1,29 +1,32 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true module CouchbaseOrm - class Error < ::StandardError - attr_reader :record - - def initialize(message = nil, record = nil) - @record = record - super(message) - end + class Error < ::StandardError + attr_reader :record + + def initialize(message = nil, record = nil) + @record = record + super(message) + end - class RecordInvalid < Error - def initialize(record = nil) - if record - @record = record - errors = @record.errors.full_messages.join(", ") - message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"couchbase.errors.messages.record_invalid") - else - message = "Record invalid" - end - super(message, record) - end + class RecordInvalid < Error + def initialize(record = nil) + if record + @record = record + errors = @record.errors.full_messages.join(', ') + message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, + default: :"couchbase.errors.messages.record_invalid") + else + message = 'Record invalid' end - class TypeMismatchError < Error; end - class RecordExists < Error; end - class EmptyNotAllowed < Error; end - class DocumentNotFound < Error; end + super(message, record) + end end + + class TypeMismatchError < Error; end + class RecordExists < Error; end + class EmptyNotAllowed < Error; end + class DocumentNotFound < Error; end + end end diff --git a/lib/couchbase-orm/extensions/string.rb b/lib/couchbase-orm/extensions/string.rb index d9395b78..3aeb37a7 100644 --- a/lib/couchbase-orm/extensions/string.rb +++ b/lib/couchbase-orm/extensions/string.rb @@ -1,27 +1,29 @@ +# frozen_string_literal: true + module CouchbaseOrm - module Extensions - module String - def reader - delete("=").sub(/\_before\_type\_cast\z/, '') - end - - def writer - sub(/\_before\_type\_cast\z/, '') + "=" - end - - def writer? - include?("=") - end - - def before_type_cast? - ends_with?("_before_type_cast") - end - - def valid_method_name? - /[@$"-]/ !~ self - end - end + module Extensions + module String + def reader + delete('=').sub(/_before_type_cast\z/, '') + end + + def writer + sub(/_before_type_cast\z/, '') + '=' + end + + def writer? + include?('=') + end + + def before_type_cast? + ends_with?('_before_type_cast') + end + + def valid_method_name? + /[@$"-]/ !~ self + end end + end end -::String.__send__(:include, CouchbaseOrm::Extensions::String) +::String.include CouchbaseOrm::Extensions::String diff --git a/lib/couchbase-orm/id_generator.rb b/lib/couchbase-orm/id_generator.rb index ff0166d9..9be5374f 100644 --- a/lib/couchbase-orm/id_generator.rb +++ b/lib/couchbase-orm/id_generator.rb @@ -1,30 +1,31 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true require 'radix/base' module CouchbaseOrm - class IdGenerator - # Using base 65 as a form of compression (reduced length of ID string) - # No escape characters are required to display these in a URL - B65 = ::Radix::Base.new(::Radix::BASE::B62 + ['-', '_', '~']) - B10 = ::Radix::Base.new(10) + class IdGenerator + # Using base 65 as a form of compression (reduced length of ID string) + # No escape characters are required to display these in a URL + B65 = ::Radix::Base.new(::Radix::BASE::B62 + ['-', '_', '~']) + B10 = ::Radix::Base.new(10) - # We don't really care about dates before this library was created - # This reduces the length of the ID significantly - Skip46Years = 1451649600 # 46.years.to_i + # We don't really care about dates before this library was created + # This reduces the length of the ID significantly + Skip46Years = 1451649600 # 46.years.to_i - # Generate a unique, orderable, ID using minimal bytes - def self.next(model) - # We are unlikely to see a clash here - now = Time.now - time = (now.to_i - Skip46Years) * 1_000_000 + now.usec + # Generate a unique, orderable, ID using minimal bytes + def self.next(model) + # We are unlikely to see a clash here + now = Time.now + time = (now.to_i - Skip46Years) * 1_000_000 + now.usec - # This makes it very very improbable that there will ever be an ID clash - # Distributed system safe! - prefix = time.to_s - tail = (rand(9999) + 1).to_s.rjust(4, '0') + # This makes it very very improbable that there will ever be an ID clash + # Distributed system safe! + prefix = time.to_s + tail = rand(1..9999).to_s.rjust(4, '0') - "#{model.class.design_document}-#{Radix.convert("#{prefix}#{tail}", B10, B65)}" - end + "#{model.class.design_document}-#{Radix.convert("#{prefix}#{tail}", B10, B65)}" end + end end diff --git a/lib/couchbase-orm/json_transcoder.rb b/lib/couchbase-orm/json_transcoder.rb index 732ddaaa..956ee43a 100644 --- a/lib/couchbase-orm/json_transcoder.rb +++ b/lib/couchbase-orm/json_transcoder.rb @@ -1,9 +1,10 @@ -require "json" +# frozen_string_literal: true + +require 'json' require 'couchbase/json_transcoder' module CouchbaseOrm class JsonTranscoder < Couchbase::JsonTranscoder - attr_reader :ignored_properties def initialize(ignored_properties: [], **options, &block) diff --git a/lib/couchbase-orm/n1ql.rb b/lib/couchbase-orm/n1ql.rb index 352ed30d..6ff78316 100644 --- a/lib/couchbase-orm/n1ql.rb +++ b/lib/couchbase-orm/n1ql.rb @@ -5,121 +5,123 @@ require 'active_support/core_ext/object/try' module CouchbaseOrm - module N1ql - extend ActiveSupport::Concern - NO_VALUE = :no_value_specified - # sanitize for injection query - def self.sanitize(value) - if value.is_a?(String) - value.gsub("'", "''").gsub("\\"){"\\\\"}.gsub('"', '\"') - elsif value.is_a?(Array) - value.map{ |v| sanitize(v) } - else - value - end + module N1ql + extend ActiveSupport::Concern + NO_VALUE = :no_value_specified + # sanitize for injection query + def self.sanitize(value) + if value.is_a?(String) + value.gsub("'", "''").gsub('\\'){ '\\\\' }.gsub('"', '\"') + elsif value.is_a?(Array) + value.map{ |v| sanitize(v) } + else + value + end + end + + module ClassMethods + # Defines a query N1QL for the model + # + # @param [Symbol, String, Array] names names of the views + # @param [Hash] options options passed to the {Couchbase::N1QL} + # + # @example Define some N1QL queries for a model + # class Post < CouchbaseOrm::Base + # n1ql :by_rating, emit_key: :rating + # end + # + # Post.by_rating do |response| + # # ... + # end + # TODO: add range keys [:startkey, :endkey] + def n1ql(name, query_fn: nil, emit_key: [], custom_order: nil, **options) + raise ArgumentError.new("#{self} already respond_to? #{name}") if self.respond_to?(name) + + emit_key = Array.wrap(emit_key) + emit_key.each do |key| + 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 = {} + method_opts[:emit_key] = emit_key + + @indexes ||= {} + @indexes[name] = method_opts + + singleton_class.__send__(:define_method, name) do |key: NO_VALUE, **opts, &result_modifier| + opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus) + values = key == NO_VALUE ? NO_VALUE : convert_values(method_opts[:emit_key], key) + current_query = run_query(method_opts[:emit_key], values, query_fn, custom_order: custom_order, + **opts.except(:include_docs, :key)) + if result_modifier + opts[:include_docs] = true + current_query.results(&result_modifier) + elsif opts[:include_docs] + current_query.results { |res| find(res) } + else + current_query.results + end + end + end + N1QL_DEFAULTS = { include_docs: true }.freeze + + # add a n1ql query and lookup method to the model for finding all records + # using a value in the supplied attr. + def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil) + n1ql_method ||= "by_#{attr}" + find_method ||= "find_#{n1ql_method}" + + validates(attr, presence: true) if validate + n1ql n1ql_method, emit_key: attr + + define_singleton_method find_method do |value| + send n1ql_method, key: [value] + end + end + + private + + def convert_values(keys, values) + return values if keys.empty? && Array.wrap(values).any? + + keys.zip(Array.wrap(values)).map do |key, value_before_type_cast| + serialize_value(key, value_before_type_cast) + end + end + + def build_where(keys, values) + where = values == NO_VALUE ? '' : keys.zip(Array.wrap(values)) + .reject { |key, value| key.nil? && value.nil? } + .map { |key, value| build_match(key, value) } + .join(' AND ') + "type=\"#{design_document}\" #{'AND ' + where unless where.blank?}" + end + + # order-by-clause ::= ORDER BY ordering-term [ ',' ordering-term ]* + # ordering-term ::= expr [ ASC | DESC ] [ NULLS ( FIRST | LAST ) ] + # see https://docs.couchbase.com/server/5.0/n1ql/n1ql-language-reference/orderby.html + def build_order(keys, descending) + keys.dup.push('meta().id').map { |k| "#{k} #{descending ? 'desc' : 'asc'}" }.join(',').to_s + end + + def build_limit(limit) + limit ? "limit #{limit}" : '' + end - module ClassMethods - # Defines a query N1QL for the model - # - # @param [Symbol, String, Array] names names of the views - # @param [Hash] options options passed to the {Couchbase::N1QL} - # - # @example Define some N1QL queries for a model - # class Post < CouchbaseOrm::Base - # n1ql :by_rating, emit_key: :rating - # end - # - # Post.by_rating do |response| - # # ... - # end - # TODO: add range keys [:startkey, :endkey] - def n1ql(name, query_fn: nil, emit_key: [], custom_order: nil, **options) - raise ArgumentError, "#{self} already respond_to? #{name}" if self.respond_to?(name) - - emit_key = Array.wrap(emit_key) - emit_key.each do |key| - 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 = {} - method_opts[:emit_key] = emit_key - - @indexes ||= {} - @indexes[name] = method_opts - - singleton_class.__send__(:define_method, name) do |key: NO_VALUE, **opts, &result_modifier| - opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus) - values = key == NO_VALUE ? NO_VALUE : convert_values(method_opts[:emit_key], key) - current_query = run_query(method_opts[:emit_key], values, query_fn, custom_order: custom_order, **opts.except(:include_docs, :key)) - if result_modifier - opts[:include_docs] = true - current_query.results &result_modifier - elsif opts[:include_docs] - current_query.results { |res| find(res) } - else - current_query.results - end - end - end - N1QL_DEFAULTS = { include_docs: true } - - # add a n1ql query and lookup method to the model for finding all records - # using a value in the supplied attr. - def index_n1ql(attr, validate: true, find_method: nil, n1ql_method: nil) - n1ql_method ||= "by_#{attr}" - find_method ||= "find_#{n1ql_method}" - - validates(attr, presence: true) if validate - n1ql n1ql_method, emit_key: attr - - define_singleton_method find_method do |value| - send n1ql_method, key: [value] - end - end - - private - - def convert_values(keys, values) - return values if keys.empty? && Array.wrap(values).any? - keys.zip(Array.wrap(values)).map do |key, value_before_type_cast| - serialize_value(key, value_before_type_cast) - end - end - - def build_where(keys, values) - where = values == NO_VALUE ? '' : keys.zip(Array.wrap(values)) - .reject { |key, value| key.nil? && value.nil? } - .map { |key, value| build_match(key, value) } - .join(" AND ") - "type=\"#{design_document}\" #{"AND " + where unless where.blank?}" - end - - # order-by-clause ::= ORDER BY ordering-term [ ',' ordering-term ]* - # ordering-term ::= expr [ ASC | DESC ] [ NULLS ( FIRST | LAST ) ] - # see https://docs.couchbase.com/server/5.0/n1ql/n1ql-language-reference/orderby.html - def build_order(keys, descending) - "#{keys.dup.push("meta().id").map { |k| "#{k} #{descending ? "desc" : "asc" }" }.join(",")}" - end - - def build_limit(limit) - limit ? "limit #{limit}" : "" - end - - def run_query(keys, values, query_fn, custom_order: nil, descending: false, limit: nil, **options) - if query_fn - N1qlProxy.new(query_fn.call(bucket, values, Couchbase::Options::Query.new(**options))) - else - bucket_name = bucket.name - where = build_where(keys, values) - order = custom_order || build_order(keys, descending) - 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 + def run_query(keys, values, query_fn, custom_order: nil, descending: false, limit: nil, **options) + if query_fn + N1qlProxy.new(query_fn.call(bucket, values, Couchbase::Options::Query.new(**options))) + else + bucket_name = bucket.name + where = build_where(keys, values) + order = custom_order || build_order(keys, descending) + 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 + end end diff --git a/lib/couchbase-orm/persistence.rb b/lib/couchbase-orm/persistence.rb index 3b5c7117..daf2c95c 100644 --- a/lib/couchbase-orm/persistence.rb +++ b/lib/couchbase-orm/persistence.rb @@ -1,268 +1,275 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true require 'active_model' require 'active_support/hash_with_indifferent_access' module CouchbaseOrm - module Persistence - extend ActiveSupport::Concern + module Persistence + extend ActiveSupport::Concern - include Encrypt + include Encrypt - included do - attribute :id, :string - end + included do + attribute :id, :string + end - module ClassMethods - def create(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr, &block) } - else - instance = new(attributes, &block) - instance.save - instance - end - end - - def create!(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, &block) } - else - instance = new(attributes, &block) - instance.save! - instance - end - end - - # Raise an error if validation failed. - def fail_validate!(document) - raise Error::RecordInvalid.new(document) - end - - # Allow classes to overwrite the default document name - # extend ActiveModel::Naming (included by ActiveModel::Model) - def design_document(name = nil) - return @design_document unless name - @design_document = name.to_s - end - - # Set a default design document - def inherited(child) - super - child.instance_eval do - @design_document = child.name.underscore - end - end + module ClassMethods + def create(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr, &block) } + else + instance = new(attributes, &block) + instance.save + instance end - - - # 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? + end + + def create!(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create!(attr, &block) } + else + instance = new(attributes, &block) + instance.save! + instance end - alias_method :new?, :new_record? - - # Returns true if this object has been destroyed, otherwise returns false. - def destroyed? - @destroyed + end + + # Raise an error if validation failed. + def fail_validate!(document) + raise Error::RecordInvalid.new(document) + end + + # Allow classes to overwrite the default document name + # extend ActiveModel::Naming (included by ActiveModel::Model) + def design_document(name = nil) + return @design_document unless name + + @design_document = name.to_s + end + + # Set a default design document + def inherited(child) + super + child.instance_eval do + @design_document = child.name.underscore end + end + 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? - !new_record? && !destroyed? - end - alias_method :exists?, :persisted? - - # Saves the model. - # - # If the model is new, a record gets created in the database, otherwise - # the existing record gets updated. - def save(**options, &block) - raise "Cannot save a destroyed document!" if destroyed? - @_with_cas = options[:with_cas] - create_or_update(**options, &block) - end + # 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? + end + alias new? new_record? - # Saves the model. - # - # If the model is new, a record gets created in the database, otherwise - # the existing record gets updated. - # - # 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, &block) - self.class.fail_validate!(self) unless self.save(**options) - self - end + # Returns true if this object has been destroyed, otherwise returns false. + def destroyed? + @destroyed + end - # Deletes the record in the database and freezes this instance to - # reflect that no changes should be made (since they can't be - # persisted). Returns the frozen instance. - # - # The record is simply removed, no callbacks are executed. - def delete(**options) - options[:cas] = @__metadata__.cas if options.delete(:with_cas) - CouchbaseOrm.logger.debug "Data - Delete #{self.id}" - self.class.collection.remove(self.id, **options) - - self.id = nil - clear_changes_information - @destroyed = true - self.freeze - self - 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? + !new_record? && !destroyed? + end + alias exists? persisted? - alias :remove :delete + # Saves the model. + # + # If the model is new, a record gets created in the database, otherwise + # the existing record gets updated. + def save(**options, &block) + raise 'Cannot save a destroyed document!' if destroyed? - # Deletes the record in the database and freezes this instance to reflect - # that no changes should be made (since they can't be persisted). - # - # There's a series of callbacks associated with #destroy. - def destroy(**options) - return self if destroyed? - raise 'model not persisted' unless persisted? + @_with_cas = options[:with_cas] + create_or_update(**options, &block) + end - run_callbacks :destroy do - destroy_associations! + # Saves the model. + # + # If the model is new, a record gets created in the database, otherwise + # the existing record gets updated. + # + # 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) + self.class.fail_validate!(self) unless self.save(**options) + self + end - options[:cas] = @__metadata__.cas if options.delete(:with_cas) - CouchbaseOrm.logger.debug "Data - Destroy #{id}" - self.class.collection.remove(id, **options) + # Deletes the record in the database and freezes this instance to + # reflect that no changes should be made (since they can't be + # persisted). Returns the frozen instance. + # + # The record is simply removed, no callbacks are executed. + def delete(**options) + options[:cas] = @__metadata__.cas if options.delete(:with_cas) + CouchbaseOrm.logger.debug "Data - Delete #{self.id}" + self.class.collection.remove(self.id, **options) + + self.id = nil + clear_changes_information + @destroyed = true + self.freeze + self + end - self.id = nil + alias remove delete - clear_changes_information - @destroyed = true - freeze - end - end - alias_method :destroy!, :destroy - - # Updates a single attribute and saves the record. - # This is especially useful for boolean flags on existing records. Also note that - # - # * Validation is skipped. - # * \Callbacks are invoked. - def update_attribute(name, value) - public_send(:"#{name}=", value) - changed? ? save(validate: false) : true - end + # Deletes the record in the database and freezes this instance to reflect + # that no changes should be made (since they can't be persisted). + # + # There's a series of callbacks associated with #destroy. + def destroy(**options) + return self if destroyed? + raise 'model not persisted' unless persisted? - def assign_attributes(hash) - hash = hash.with_indifferent_access if hash.is_a?(Hash) - super(hash.except("type")) - end + run_callbacks :destroy do + destroy_associations! - # 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) - assign_attributes(hash) - save - end - alias_method :update_attributes, :update + options[:cas] = @__metadata__.cas if options.delete(:with_cas) + CouchbaseOrm.logger.debug "Data - Destroy #{id}" + self.class.collection.remove(id, **options) - # Updates its receiver just like #update but calls #save! instead - # of +save+, so an exception is raised if the record is invalid and saving will fail. - def update!(hash) - assign_attributes(hash) # Assign attributes is provided by ActiveModel::AttributeAssignment - save! - end - alias_method :update_attributes!, :update! - - # 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) - raise "unable to update columns, model not persisted" unless id - - assign_attributes(hash) - - options = {extended: true} - options[:cas] = @__metadata__.cas if with_cas - - # There is a limit of 16 subdoc operations per request - resp = if hash.length <= 16 - self.class.collection.mutate_in( - id, - hash.map { |k, v| Couchbase::MutateInSpec.replace(k.to_s, v) } - ) - else - # Fallback to writing the whole document - 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 + self.id = nil - # Ensure the model is up to date - @__metadata__.cas = resp.cas + clear_changes_information + @destroyed = true + freeze + end + end + alias destroy! destroy + + # Updates a single attribute and saves the record. + # This is especially useful for boolean flags on existing records. Also note that + # + # * Validation is skipped. + # * \Callbacks are invoked. + def update_attribute(name, value) + public_send(:"#{name}=", value) + changed? ? save(validate: false) : true + end - changes_applied - self - end + def assign_attributes(hash) + hash = hash.with_indifferent_access if hash.is_a?(Hash) + super(hash.except('type')) + end - # Reloads the record from the database. - # - # This method finds record by its key and modifies the receiver in-place: - def reload - raise "unable to reload, model not persisted" unless id + # 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) + assign_attributes(hash) + save + end + alias update_attributes update - CouchbaseOrm.logger.debug "Data - Get #{id}" - resp = self.class.collection.get!(id) - assign_attributes(decode_encrypted_attributes(resp.content.except("id"))) # API return a nil id - @__metadata__.cas = resp.cas + # Updates its receiver just like #update but calls #save! instead + # of +save+, so an exception is raised if the record is invalid and saving will fail. + def update!(hash) + assign_attributes(hash) # Assign attributes is provided by ActiveModel::AttributeAssignment + save! + end + alias update_attributes! update! + + # 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) + raise 'unable to update columns, model not persisted' unless id + + assign_attributes(hash) + + options = {extended: true} + options[:cas] = @__metadata__.cas if with_cas + + # There is a limit of 16 subdoc operations per request + resp = if hash.length <= 16 + self.class.collection.mutate_in( + id, + hash.map { |k, v| Couchbase::MutateInSpec.replace(k.to_s, v) } + ) + else + # Fallback to writing the whole document + 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 + @__metadata__.cas = resp.cas + + changes_applied + self + end - reset_associations - clear_changes_information - self - end + # Reloads the record from the database. + # + # This method finds record by its key and modifies the receiver in-place: + def reload + raise 'unable to reload, model not persisted' unless id - # Updates the TTL of the document - def touch(**options) - CouchbaseOrm.logger.debug "Data - Touch #{id}" - _res = self.class.collection.touch(id, async: false, **options) - @__metadata__.cas = resp.cas - self - end + CouchbaseOrm.logger.debug "Data - Get #{id}" + resp = self.class.collection.get!(id) + assign_attributes(decode_encrypted_attributes(resp.content.except('id'))) # API return a nil id + @__metadata__.cas = resp.cas - def create_or_update(**, &block) - self.new_record? ? _create_record(&block) : _update_record(&block) - end + reset_associations + clear_changes_information + self + end + + # Updates the TTL of the document + def touch(**options) + CouchbaseOrm.logger.debug "Data - Touch #{id}" + _res = self.class.collection.touch(id, async: false, **options) + @__metadata__.cas = resp.cas + self + end - def _update_record(*, &block) - return true unless changed? || self.class.attribute_types.any? { |_, type| type.is_a?(CouchbaseOrm::Types::Nested) || type.is_a?(CouchbaseOrm::Types::Array) } + def create_or_update(**, &block) + self.new_record? ? _create_record(&block) : _update_record(&block) + end + + def _update_record(*) + return true unless changed? || self.class.attribute_types.any? { |_, type| + type.is_a?(CouchbaseOrm::Types::Nested) || type.is_a?(CouchbaseOrm::Types::Array) + } - run_callbacks :update do - run_callbacks :save do - options = {} - options[:cas] = @__metadata__.cas if @_with_cas - 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)) + run_callbacks :update do + run_callbacks :save do + options = {} + options[:cas] = @__metadata__.cas if @_with_cas + 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__.cas = resp.cas + # Ensure the model is up to date + @__metadata__.cas = resp.cas - changes_applied - true - end - end + changes_applied + true end + end + end - def _create_record(*, &block) - run_callbacks :create do - run_callbacks :save do - 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(self.id, serialized_attributes.except("id").merge(type: self.class.design_document), Couchbase::Options::Upsert.new) + def _create_record(*) + run_callbacks :create do + run_callbacks :save do + 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(self.id, + serialized_attributes.except('id').merge(type: self.class.design_document), Couchbase::Options::Upsert.new) - # Ensure the model is up to date - @__metadata__.cas = resp.cas + # Ensure the model is up to date + @__metadata__.cas = resp.cas - changes_applied - true - end - end + changes_applied + true end + end end + end end diff --git a/lib/couchbase-orm/proxies/bucket_proxy.rb b/lib/couchbase-orm/proxies/bucket_proxy.rb index 259ecd1e..055d3532 100644 --- a/lib/couchbase-orm/proxies/bucket_proxy.rb +++ b/lib/couchbase-orm/proxies/bucket_proxy.rb @@ -1,36 +1,37 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true require 'couchbase-orm/proxies/n1ql_proxy' module CouchbaseOrm - class BucketProxy - def initialize(proxyfied) - raise ArgumentError, "Must proxy a non nil object" if proxyfied.nil? + class BucketProxy + def initialize(proxyfied) + raise ArgumentError.new('Must proxy a non nil object') if proxyfied.nil? - @proxyfied = proxyfied + @proxyfied = proxyfied - self.class.define_method(:n1ql) do - N1qlProxy.new(@proxyfied.n1ql) - end + self.class.define_method(:n1ql) do + N1qlProxy.new(@proxyfied.n1ql) + end - self.class.define_method(:view) do |design, view, **opts, &block| - @results = nil if @current_query != "#{design}_#{view}" - @current_query = "#{design}_#{view}" - return @results if @results + self.class.define_method(:view) do |design, view, **opts, &block| + @results = nil if @current_query != "#{design}_#{view}" + @current_query = "#{design}_#{view}" + return @results if @results - CouchbaseOrm.logger.debug "View - #{design} #{view}" - @results = ResultsProxy.new(@proxyfied.send(:view, design, view, **opts, &block)) - 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 + CouchbaseOrm.logger.debug "View - #{design} #{view}" + @results = ResultsProxy.new(@proxyfied.send(:view, design, view, **opts, &block)) + 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 end diff --git a/lib/couchbase-orm/proxies/collection_proxy.rb b/lib/couchbase-orm/proxies/collection_proxy.rb index 0d1c94bc..5503fdf0 100644 --- a/lib/couchbase-orm/proxies/collection_proxy.rb +++ b/lib/couchbase-orm/proxies/collection_proxy.rb @@ -1,69 +1,71 @@ -require "couchbase" +# frozen_string_literal: true + +require 'couchbase' module CouchbaseOrm - class CollectionProxy + 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)) - end + def get(id, **options) + @proxyfied.get(id, Couchbase::Options::Get.new(**options)) + rescue Couchbase::Error::DocumentNotFound + nil + 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 - 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 + result + end - def get_multi(*ids, **options) - result = @proxyfied.get_multi(*ids, Couchbase::Options::GetMulti.new(**options)) - result.reject(&:error) - 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)) + end - def remove(id, **options) - @proxyfied.remove(id, Couchbase::Options::Remove.new(**options)) - rescue Couchbase::Error::DocumentNotFound - nil - end + def remove(id, **options) + @proxyfied.remove(id, Couchbase::Options::Remove.new(**options)) + rescue Couchbase::Error::DocumentNotFound + nil + end - def remove_multi!(ids, **options) - result = @proxyfied.remove_multi(ids, Couchbase::Options::RemoveMulti.new(**options)) - first_result_with_error = result.find(&:error) - if first_result_with_error - raise CouchbaseOrm::Error::DocumentNotFound - end - result - end + def remove_multi!(ids, **options) + result = @proxyfied.remove_multi(ids, Couchbase::Options::RemoveMulti.new(**options)) + first_result_with_error = result.find(&:error) + if first_result_with_error + raise CouchbaseOrm::Error::DocumentNotFound + end - def remove_multi(ids, **options) - result = @proxyfied.remove_multi(ids, Couchbase::Options::RemoveMulti.new(**options)) - result.reject(&:error) - end + result + end + def remove_multi(ids, **options) + result = @proxyfied.remove_multi(ids, Couchbase::Options::RemoveMulti.new(**options)) + result.reject(&:error) + end + def initialize(proxyfied) + raise 'Must proxy a non nil object' if proxyfied.nil? + + @proxyfied = proxyfied + 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 + 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 db4e9363..3a32d76b 100644 --- a/lib/couchbase-orm/proxies/n1ql_proxy.rb +++ b/lib/couchbase-orm/proxies/n1ql_proxy.rb @@ -1,40 +1,41 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true require 'couchbase-orm/proxies/results_proxy' module CouchbaseOrm - class N1qlProxy - def initialize(proxyfied) - @proxyfied = proxyfied - - self.class.define_method(:results) do |*params, &block| - @results = nil if @current_query != self.to_s - @current_query = self.to_s - return @results if @results - - CouchbaseOrm.logger.debug { 'Query - ' + self.to_s } - - 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 - @proxyfied.to_s.tr("\n", ' ') - end - - proxyfied.public_methods.each do |method| - next if self.public_methods.include?(method) - - self.class.define_method(method) do |*params, &block| - ret = @proxyfied.send(method, *params, &block) - ret.is_a?(@proxyfied.class) ? self : ret - end - end - end + class N1qlProxy + def initialize(proxyfied) + @proxyfied = proxyfied + + self.class.define_method(:results) do |*_params, &block| + @results = nil if @current_query != self.to_s + @current_query = self.to_s + return @results if @results + + CouchbaseOrm.logger.debug { 'Query - ' + self.to_s } + + results = @proxyfied.rows + results = results.map { |r| block.call(r) } if block + @results = ResultsProxy.new(results.to_a) + end - def method_missing(m, *args, &block) - self.results.send(m, *args, &block) + self.class.define_method(:to_s) do + @proxyfied.to_s.tr("\n", ' ') + end + + proxyfied.public_methods.each do |method| + next if self.public_methods.include?(method) + + self.class.define_method(method) do |*params, &block| + ret = @proxyfied.send(method, *params, &block) + ret.is_a?(@proxyfied.class) ? self : ret end + end + end + + def method_missing(m, *args, &block) + self.results.send(m, *args, &block) end + end end diff --git a/lib/couchbase-orm/proxies/results_proxy.rb b/lib/couchbase-orm/proxies/results_proxy.rb index 27d2897c..a8d10faf 100644 --- a/lib/couchbase-orm/proxies/results_proxy.rb +++ b/lib/couchbase-orm/proxies/results_proxy.rb @@ -1,23 +1,24 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true module CouchbaseOrm - class ResultsProxy - def initialize(proxyfied) - @proxyfied = proxyfied - - raise ArgumentError, "Proxyfied object must respond to :to_a" unless @proxyfied.respond_to?(:to_a) + class ResultsProxy + def initialize(proxyfied) + @proxyfied = proxyfied - proxyfied.public_methods.each do |method| - next if self.public_methods.include?(method) + raise ArgumentError.new('Proxyfied object must respond to :to_a') unless @proxyfied.respond_to?(:to_a) - self.class.define_method(method) do |*params, &block| - @proxyfied.send(method, *params, &block) - end - end - end - - def method_missing(m, *args, &block) - @proxyfied.to_a.send(m, *args, &block) + proxyfied.public_methods.each do |method| + next if self.public_methods.include?(method) + + self.class.define_method(method) do |*params, &block| + @proxyfied.send(method, *params, &block) end + end + end + + def method_missing(m, *args, &block) + @proxyfied.to_a.send(m, *args, &block) end + end end diff --git a/lib/couchbase-orm/railtie.rb b/lib/couchbase-orm/railtie.rb index 4d6d5706..07eed32d 100644 --- a/lib/couchbase-orm/railtie.rb +++ b/lib/couchbase-orm/railtie.rb @@ -1,4 +1,5 @@ -# encoding: utf-8 +# frozen_string_literal: true + # # Author:: Couchbase # Copyright:: 2012 Couchbase, Inc. @@ -20,68 +21,65 @@ require 'yaml' require 'couchbase-orm/base' -module Rails #:nodoc: - module Couchbase #:nodoc: - class Railtie < Rails::Railtie #:nodoc: - config.couchbase_orm = ActiveSupport::OrderedOptions.new - config.couchbase_orm.ensure_design_documents = true +module Rails # :nodoc: + module Couchbase # :nodoc: + class Railtie < Rails::Railtie # :nodoc: + config.couchbase_orm = ActiveSupport::OrderedOptions.new + config.couchbase_orm.ensure_design_documents = true - # Maping of rescued exceptions to HTTP responses - # - # @example - # railtie.rescue_responses - # - # @return [Hash] rescued responses - def self.rescue_responses - { - } - end + # Maping of rescued exceptions to HTTP responses + # + # @example + # railtie.rescue_responses + # + # @return [Hash] rescued responses + def self.rescue_responses + { + } + end - config.send(:app_generators).orm :couchbase_orm, :migration => false + config.send(:app_generators).orm :couchbase_orm, migration: false - if config.action_dispatch.rescue_responses - config.action_dispatch.rescue_responses.merge!(rescue_responses) - end + config.action_dispatch.rescue_responses&.merge!(rescue_responses) - initializer 'couchbase_orm.setup_connection_config' do - CouchbaseOrm::Connection.config = Rails.application.config_for(:couchbase) - 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 - # alert to create one. - initializer 'couchbase.warn_configuration_missing' do - unless ARGV.include?('couchbase:config') - config.after_initialize do - unless Rails.root.join('config', 'couchbase.yml').file? - puts "\nCouchbase config not found. Create a config file at: config/couchbase.yml" - puts "to generate one run: rails generate couchbase:config\n\n" - end - end - end + # After initialization we will warn the user if we can't find a couchbase.yml and + # alert to create one. + initializer 'couchbase.warn_configuration_missing' do + unless ARGV.include?('couchbase:config') + config.after_initialize do + unless Rails.root.join('config', 'couchbase.yml').file? + puts "\nCouchbase config not found. Create a config file at: config/couchbase.yml" + puts "to generate one run: rails generate couchbase:config\n\n" end + end + end + end - # Set the proper error types for Rails. NotFound errors should be - # 404s and not 500s, validation errors are 422s. - initializer 'couchbase.load_http_errors' do |app| - config.after_initialize do - unless config.action_dispatch.rescue_responses - ActionDispatch::ShowExceptions.rescue_responses.update(Railtie.rescue_responses) - end - end - end + # Set the proper error types for Rails. NotFound errors should be + # 404s and not 500s, validation errors are 422s. + initializer 'couchbase.load_http_errors' do |_app| + config.after_initialize do + unless config.action_dispatch.rescue_responses + ActionDispatch::ShowExceptions.rescue_responses.update(Railtie.rescue_responses) + end + end + end - # Check (and upgrade if needed) all design documents - config.after_initialize do |app| - if config.couchbase_orm.ensure_design_documents - begin - ::CouchbaseOrm::Base.descendants.each do |model| - model.ensure_design_document! - end - rescue ::MTLibcouchbase::Error::Timedout, ::MTLibcouchbase::Error::ConnectError, ::MTLibcouchbase::Error::NetworkError - # skip connection errors for now - end - end - end + # Check (and upgrade if needed) all design documents + config.after_initialize do |_app| + if config.couchbase_orm.ensure_design_documents + begin + ::CouchbaseOrm::Base.descendants.each(&:ensure_design_document!) + rescue ::MTLibcouchbase::Error::Timedout, ::MTLibcouchbase::Error::ConnectError, + ::MTLibcouchbase::Error::NetworkError + # skip connection errors for now + end end + end end + end end diff --git a/lib/couchbase-orm/relation.rb b/lib/couchbase-orm/relation.rb index 19d72890..a706ae99 100644 --- a/lib/couchbase-orm/relation.rb +++ b/lib/couchbase-orm/relation.rb @@ -1,239 +1,251 @@ -module CouchbaseOrm - module Relation - extend ActiveSupport::Concern - - class CouchbaseOrm_Relation - def initialize(model:, where: where = nil, order: order = nil, limit: limit = nil, _not: _not = false) - CouchbaseOrm::logger.debug "CouchbaseOrm_Relation init: #{model} where:#{where.inspect} not:#{_not.inspect} order:#{order.inspect} limit: #{limit}" - @model = model - @limit = limit - @where = [] - @order = {} - @order = merge_order(**order) if order - @where = merge_where(where, _not) if where - CouchbaseOrm::logger.debug "- #{to_s}" - end - - def to_s - "CouchbaseOrm_Relation: #{@model} where:#{@where.inspect} order:#{@order.inspect} limit: #{@limit}" - end - - def to_n1ql - bucket_name = @model.bucket.name - where = build_where - order = build_order - limit = build_limit - "select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}" - end - - def execute(n1ql_query) - result = @model.cluster.query(n1ql_query, Couchbase::Options::Query.new(scan_consistency: :request_plus)) - CouchbaseOrm.logger.debug { "Relation query: #{n1ql_query} return #{result.rows.to_a.length} rows" } - N1qlProxy.new(result) - end - - def query - CouchbaseOrm::logger.debug("Query: #{self}") - n1ql_query = to_n1ql - execute(n1ql_query) - end - - def update_all(**cond) - bucket_name = @model.bucket.name - where = build_where - limit = build_limit - update = build_update(**cond) - n1ql_query = "update `#{bucket_name}` set #{update} where #{where} #{limit}" - execute(n1ql_query) - end +# frozen_string_literal: true - def ids - query.to_a - end - - def first - result = @model.cluster.query(self.limit(1).to_n1ql, Couchbase::Options::Query.new(scan_consistency: :request_plus)) - first_id = result.rows.to_a.first - @model.find(first_id) if first_id - end - - def last - result = @model.cluster.query(to_n1ql, Couchbase::Options::Query.new(scan_consistency: :request_plus)) - last_id = result.rows.to_a.last - @model.find(last_id) if last_id - end - - def count - query.count - end - - def empty? - limit(1).count == 0 - end - - def pluck(*fields) - map do |model| - if fields.length == 1 - model.send(fields.first) - else - fields.map do |field| - model.send(field) - end - end - end - end - - alias :size :count - alias :length :count - - def to_ary - ids = query.results - return [] if ids.empty? - Array(ids && @model.find(ids)) - end - - alias :to_a :to_ary - - delegate :each, :map, :collect, :find, :filter, :reduce, :to => :to_ary - - def [](*args) - to_ary[*args] - end - - def delete_all - CouchbaseOrm::logger.debug{ "Delete all: #{self}" } - ids = query.to_a - @model.collection.remove_multi(ids) unless ids.empty? - end - - def where(string_cond=nil, **conds) - CouchbaseOrm_Relation.new(**initializer_arguments.merge(where: merge_where(conds)+string_where(string_cond))) - end - - def find_by(**conds) - CouchbaseOrm_Relation.new(**initializer_arguments.merge(where: merge_where(conds))).first - end - - def not(**conds) - CouchbaseOrm_Relation.new(**initializer_arguments.merge(where: merge_where(conds, _not: true))) - end - - def order(*lorder, **horder) - CouchbaseOrm_Relation.new(**initializer_arguments.merge(order: merge_order(*lorder, **horder))) - end - - def limit(limit) - CouchbaseOrm_Relation.new(**initializer_arguments.merge(limit: limit)) - end - - def all - CouchbaseOrm_Relation.new(**initializer_arguments) - end - - def scoping - scopes = (Thread.current[@model.name] ||= []) - scopes.push(self) - result = yield - ensure - scopes.pop - result - end - - private - - def build_limit - @limit ? "limit #{@limit}" : "" - end - - def initializer_arguments - { model: @model, order: @order, where: @where, limit: @limit } - end - - def merge_order(*lorder, **horder) - raise ArgumentError, "invalid order passed by list: #{lorder.inspect}, must be symbols" unless lorder.all? { |o| o.is_a? Symbol } - raise ArgumentError, "Invalid order passed by hash: #{horder.inspect}, must be symbol -> :asc|:desc" unless horder.all? { |k, v| k.is_a?(Symbol) && [:asc, :desc].include?(v) } - @order - .merge(Array.wrap(lorder).map{ |o| [o, :asc] }.to_h) - .merge(horder) - end - - def merge_where(conds, _not = false) - @where + (_not ? conds.to_a.map{|k,v|[k,v,:not]} : conds.to_a) - end - - def string_where(string_cond, _not = false) - return [] unless string_cond - cond = "(#{string_cond})" - [(_not ? [nil, cond, :not] : [nil, cond])] - end - - def build_order - order = @order.map do |key, value| - "#{key} #{value}" - end.join(", ") - order.empty? ? "meta().id" : order - end - - def build_where - build_conds([[:type, @model.design_document]] + @where) - end - - def build_conds(conds) - conds.map do |key, value, opt| - if key - opt == :not ? - @model.build_not_match(key, value) : - @model.build_match(key, value) - else - value - end - end.join(" AND ") - end - - def build_update(**cond) - cond.map do |key, value| - for_clause="" - if value.is_a?(Hash) && value[:_for] - path_clause = value.delete(:_for) - var_clause = path_clause.to_s.split(".").last.singularize - - _when = value.delete(:_when) - when_clause = _when ? build_conds(_when.to_a) : "" - - _set = value.delete(:_set) - value = _set if _set - - for_clause = " for #{var_clause} in #{path_clause} when #{when_clause} end" - end - if value.is_a?(Hash) - value.map do |k, v| - "#{key}.#{k} = #{v}" - end.join(", ") + for_clause - else - "#{key} = #{@model.quote(value)}#{for_clause}" - end - end.join(", ") - end - - def method_missing(method, *args, &block) - if @model.respond_to?(method) - scoping { - @model.public_send(method, *args, &block) - } - else - super - end - end +module CouchbaseOrm + module Relation + extend ActiveSupport::Concern + + class CouchbaseOrm_Relation + def initialize(model:, where: where = nil, order: order = nil, limit: limit = nil, _not: _not = false) + CouchbaseOrm.logger.debug "CouchbaseOrm_Relation init: #{model} where:#{where.inspect} not:#{_not.inspect} order:#{order.inspect} limit: #{limit}" + @model = model + @limit = limit + @where = [] + @order = {} + @order = merge_order(**order) if order + @where = merge_where(where, _not) if where + CouchbaseOrm.logger.debug "- #{self}" + end + + def to_s + "CouchbaseOrm_Relation: #{@model} where:#{@where.inspect} order:#{@order.inspect} limit: #{@limit}" + end + + def to_n1ql + bucket_name = @model.bucket.name + where = build_where + order = build_order + limit = build_limit + "select raw meta().id from `#{bucket_name}` where #{where} order by #{order} #{limit}" + end + + def execute(n1ql_query) + result = @model.cluster.query(n1ql_query, Couchbase::Options::Query.new(scan_consistency: :request_plus)) + CouchbaseOrm.logger.debug { "Relation query: #{n1ql_query} return #{result.rows.to_a.length} rows" } + N1qlProxy.new(result) + end + + def query + CouchbaseOrm.logger.debug("Query: #{self}") + n1ql_query = to_n1ql + execute(n1ql_query) + end + + def update_all(**cond) + bucket_name = @model.bucket.name + where = build_where + limit = build_limit + update = build_update(**cond) + n1ql_query = "update `#{bucket_name}` set #{update} where #{where} #{limit}" + execute(n1ql_query) + end + + def ids + query.to_a + end + + def first + result = @model.cluster.query(self.limit(1).to_n1ql, + Couchbase::Options::Query.new(scan_consistency: :request_plus)) + first_id = result.rows.to_a.first + @model.find(first_id) if first_id + end + + def last + result = @model.cluster.query(to_n1ql, Couchbase::Options::Query.new(scan_consistency: :request_plus)) + last_id = result.rows.to_a.last + @model.find(last_id) if last_id + end + + def count + query.count + end + + def empty? + limit(1).count.zero? + end + + def pluck(*fields) + map do |model| + if fields.length == 1 + model.send(fields.first) + else + fields.map do |field| + model.send(field) + end + end end + end + + alias size count + alias length count + + def to_ary + ids = query.results + return [] if ids.empty? + + Array(ids && @model.find(ids)) + end + + alias to_a to_ary + + delegate :each, :map, :collect, :find, :filter, :reduce, to: :to_ary + + def [](*args) + to_ary[*args] + end + + def delete_all + CouchbaseOrm.logger.debug{ "Delete all: #{self}" } + ids = query.to_a + @model.collection.remove_multi(ids) unless ids.empty? + end + + def where(string_cond = nil, **conds) + CouchbaseOrm_Relation.new(**initializer_arguments.merge(where: merge_where(conds) + string_where(string_cond))) + end + + def find_by(**conds) + CouchbaseOrm_Relation.new(**initializer_arguments.merge(where: merge_where(conds))).first + end + + def not(**conds) + CouchbaseOrm_Relation.new(**initializer_arguments.merge(where: merge_where(conds, _not: true))) + end + + def order(*lorder, **horder) + CouchbaseOrm_Relation.new(**initializer_arguments.merge(order: merge_order(*lorder, **horder))) + end + + def limit(limit) + CouchbaseOrm_Relation.new(**initializer_arguments.merge(limit: limit)) + end + + def all + CouchbaseOrm_Relation.new(**initializer_arguments) + end + + def scoping + scopes = (Thread.current[@model.name] ||= []) + scopes.push(self) + result = yield + ensure + scopes.pop + result + end + + private + + def build_limit + @limit ? "limit #{@limit}" : '' + end + + def initializer_arguments + { model: @model, order: @order, where: @where, limit: @limit } + end + + def merge_order(*lorder, **horder) + raise ArgumentError.new("invalid order passed by list: #{lorder.inspect}, must be symbols") unless lorder.all? { |o| + o.is_a? Symbol + } + raise ArgumentError.new("Invalid order passed by hash: #{horder.inspect}, must be symbol -> :asc|:desc") unless horder.all? { |k, v| + k.is_a?(Symbol) && [ +:asc, :desc +].include?(v) + } + + @order + .merge(Array.wrap(lorder).map{ |o| [o, :asc] }.to_h) + .merge(horder) + end + + def merge_where(conds, _not = false) + @where + (_not ? conds.to_a.map{ |k, v| [k, v, :not] } : conds.to_a) + end + + def string_where(string_cond, _not = false) + return [] unless string_cond + + cond = "(#{string_cond})" + [(_not ? [nil, cond, :not] : [nil, cond])] + end + + def build_order + order = @order.map do |key, value| + "#{key} #{value}" + end.join(', ') + order.empty? ? 'meta().id' : order + end + + def build_where + build_conds([[:type, @model.design_document]] + @where) + end + + def build_conds(conds) + conds.map do |key, value, opt| + if key + opt == :not ? + @model.build_not_match(key, value) : + @model.build_match(key, value) + else + value + end + end.join(' AND ') + end + + def build_update(**cond) + cond.map do |key, value| + for_clause = '' + if value.is_a?(Hash) && value[:_for] + path_clause = value.delete(:_for) + var_clause = path_clause.to_s.split('.').last.singularize + + _when = value.delete(:_when) + when_clause = _when ? build_conds(_when.to_a) : '' + + _set = value.delete(:_set) + value = _set if _set + + for_clause = " for #{var_clause} in #{path_clause} when #{when_clause} end" + end + if value.is_a?(Hash) + value.map do |k, v| + "#{key}.#{k} = #{v}" + end.join(', ') + for_clause + else + "#{key} = #{@model.quote(value)}#{for_clause}" + end + end.join(', ') + end + + def method_missing(method, *args, &block) + if @model.respond_to?(method) + scoping { + @model.public_send(method, *args, &block) + } + else + super + end + end + end - module ClassMethods - def relation - Thread.current[self.name]&.last || CouchbaseOrm_Relation.new(model: self) - end + module ClassMethods + def relation + Thread.current[self.name]&.last || CouchbaseOrm_Relation.new(model: self) + end - delegate :ids, :update_all, :delete_all, :count, :empty?, :filter, :reduce, :find_by, to: :all + delegate :ids, :update_all, :delete_all, :count, :empty?, :filter, :reduce, :find_by, to: :all - delegate :where, :not, :order, :limit, :all, to: :relation - end + delegate :where, :not, :order, :limit, :all, to: :relation end + end end diff --git a/lib/couchbase-orm/types.rb b/lib/couchbase-orm/types.rb index c97db5ad..9909aae6 100644 --- a/lib/couchbase-orm/types.rb +++ b/lib/couchbase-orm/types.rb @@ -1,10 +1,12 @@ -require "couchbase-orm/types/date" -require "couchbase-orm/types/date_time" -require "couchbase-orm/types/timestamp" -require "couchbase-orm/types/array" -require "couchbase-orm/types/nested" -require "couchbase-orm/types/encrypted" -require "couchbase-orm/types/hash" +# frozen_string_literal: true + +require 'couchbase-orm/types/date' +require 'couchbase-orm/types/date_time' +require 'couchbase-orm/types/timestamp' +require 'couchbase-orm/types/array' +require 'couchbase-orm/types/nested' +require 'couchbase-orm/types/encrypted' +require 'couchbase-orm/types/hash' if ActiveModel::VERSION::MAJOR <= 6 # In Rails 5, the type system cannot allow overriding the default types @@ -20,4 +22,3 @@ ActiveModel::Type.register(:nested, CouchbaseOrm::Types::Nested) ActiveModel::Type.register(:encrypted, CouchbaseOrm::Types::Encrypted) ActiveModel::Type.register(:hash, CouchbaseOrm::Types::Hash) - diff --git a/lib/couchbase-orm/types/array.rb b/lib/couchbase-orm/types/array.rb index a8dd7fe5..0abc5b73 100644 --- a/lib/couchbase-orm/types/array.rb +++ b/lib/couchbase-orm/types/array.rb @@ -1,32 +1,33 @@ +# frozen_string_literal: true + module CouchbaseOrm - module Types - class Array < ActiveModel::Type::Value - attr_reader :type_class - attr_reader :model_class + module Types + class Array < ActiveModel::Type::Value + attr_reader :type_class, :model_class - def initialize(type: nil) - if type.is_a?(Class) && type < CouchbaseOrm::NestedDocument - @model_class = type - @type_class = CouchbaseOrm::Types::Nested.new(type: @model_class) - else - @type_class = ActiveModel::Type.registry.lookup(type) - end - super() - end + def initialize(type: nil) + if type.is_a?(Class) && type < CouchbaseOrm::NestedDocument + @model_class = type + @type_class = CouchbaseOrm::Types::Nested.new(type: @model_class) + else + @type_class = ActiveModel::Type.registry.lookup(type) + end + super() + end - def cast(values) - return [] if values.nil? + def cast(values) + return [] if values.nil? - raise ArgumentError, "#{values.inspect} must be an array" unless values.is_a?(::Array) - - values.map(&@type_class.method(:cast)) - end - - def serialize(values) - return [] if values.nil? + raise ArgumentError.new("#{values.inspect} must be an array") unless values.is_a?(::Array) - values.map(&@type_class.method(:serialize)) - end - end + values.map(&@type_class.method(:cast)) + end + + def serialize(values) + return [] if values.nil? + + values.map(&@type_class.method(:serialize)) + end end + end end diff --git a/lib/couchbase-orm/types/date.rb b/lib/couchbase-orm/types/date.rb index 2e836cf8..eadf906e 100644 --- a/lib/couchbase-orm/types/date.rb +++ b/lib/couchbase-orm/types/date.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module CouchbaseOrm - module Types - class Date < ActiveModel::Type::Date - def serialize(value) - value&.iso8601 - end - end + 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 index 37bdaff0..7d458241 100644 --- a/lib/couchbase-orm/types/date_time.rb +++ b/lib/couchbase-orm/types/date_time.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + module CouchbaseOrm - module Types - class DateTime < ActiveModel::Type::DateTime - def cast(value) - value = Time.at(value) if value.is_a?(Float) || value.is_a?(Integer) - super(value)&.utc - end + module Types + class DateTime < ActiveModel::Type::DateTime + def cast(value) + value = Time.at(value) if value.is_a?(Float) || value.is_a?(Integer) + super(value)&.utc + end - def serialize(value) - value&.iso8601(@precision) - end - end + def serialize(value) + value&.iso8601(@precision) + end end + end end diff --git a/lib/couchbase-orm/types/encrypted.rb b/lib/couchbase-orm/types/encrypted.rb index b6bf983d..cd9b3632 100644 --- a/lib/couchbase-orm/types/encrypted.rb +++ b/lib/couchbase-orm/types/encrypted.rb @@ -1,17 +1,20 @@ +# frozen_string_literal: true + module CouchbaseOrm - module Types - class Encrypted < ActiveModel::Type::Value - attr_reader :alg + module Types + class Encrypted < ActiveModel::Type::Value + attr_reader :alg + + def initialize(alg: 'CB_MOBILE_CUSTOM') + @alg = alg + super() + end - def initialize(alg: "CB_MOBILE_CUSTOM") - @alg = alg - super() - end + def serialize(value) + return nil if value.nil? - def serialize(value) - return nil if value.nil? - value - end - end + value + end end + end end diff --git a/lib/couchbase-orm/types/hash.rb b/lib/couchbase-orm/types/hash.rb index 9b0ee67e..a64ab061 100644 --- a/lib/couchbase-orm/types/hash.rb +++ b/lib/couchbase-orm/types/hash.rb @@ -1,21 +1,23 @@ +# frozen_string_literal: true + module CouchbaseOrm - module Types - class Hash < ActiveModel::Type::Value - def cast(value) - return nil if value.nil? - return value if value.is_a?(ActiveSupport::HashWithIndifferentAccess) - return value.with_indifferent_access if value.is_a?(::Hash) + module Types + class Hash < ActiveModel::Type::Value + def cast(value) + return nil if value.nil? + return value if value.is_a?(ActiveSupport::HashWithIndifferentAccess) + return value.with_indifferent_access if value.is_a?(::Hash) - raise ArgumentError, "Hash: #{value.inspect} (#{value.class}) is not supported for cast" - end + raise ArgumentError.new("Hash: #{value.inspect} (#{value.class}) is not supported for cast") + end - def serialize(value) - return nil if value.nil? - return value.as_json if value.is_a?(ActiveSupport::HashWithIndifferentAccess) - return value.with_indifferent_access.as_json if value.is_a?(::Hash) + def serialize(value) + return nil if value.nil? + return value.as_json if value.is_a?(ActiveSupport::HashWithIndifferentAccess) + return value.with_indifferent_access.as_json if value.is_a?(::Hash) - raise ArgumentError, "Hash: #{value.inspect} (#{value.class}) is not supported for serialize" - end - end + raise ArgumentError.new("Hash: #{value.inspect} (#{value.class}) is not supported for serialize") + end end + end end diff --git a/lib/couchbase-orm/types/nested.rb b/lib/couchbase-orm/types/nested.rb index 8896d088..f8b231e9 100644 --- a/lib/couchbase-orm/types/nested.rb +++ b/lib/couchbase-orm/types/nested.rb @@ -1,43 +1,45 @@ -class NestedValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - if value.is_a?(Array) - record.errors.add attribute, (options[:message] || "is invalid") unless value.map(&:valid?).all? - else - record.errors.add attribute, (options[:message] || "is invalid") unless - value.nil? || value.valid? - end +# frozen_string_literal: true +class NestedValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if value.is_a?(Array) + record.errors.add attribute, (options[:message] || 'is invalid') unless value.map(&:valid?).all? + else + record.errors.add attribute, (options[:message] || 'is invalid') unless + value.nil? || value.valid? end + end end module CouchbaseOrm - module Types - class Nested < ActiveModel::Type::Value - attr_reader :model_class - - def initialize(type:) - raise ArgumentError, "type is nil" if type.nil? - raise ArgumentError, "type is not a class : #{type.inspect}" unless type.is_a?(Class) - - @model_class = type - super() - end - - def cast(value) - return nil if value.nil? - return value if value.is_a?(@model_class) - return @model_class.new(value) if value.is_a?(::Hash) - - raise ArgumentError, "Nested: #{value.inspect} (#{value.class}) is not supported for cast" - end - - def serialize(value) - return nil if value.nil? - value = @model_class.new(value) if value.is_a?(::Hash) - return value.send(:serialized_attributes) if value.is_a?(@model_class) - - raise ArgumentError, "Nested: #{value.inspect} (#{value.class}) is not supported for serialization" - end - end + module Types + class Nested < ActiveModel::Type::Value + attr_reader :model_class + + def initialize(type:) + raise ArgumentError.new('type is nil') if type.nil? + raise ArgumentError.new("type is not a class : #{type.inspect}") unless type.is_a?(Class) + + @model_class = type + super() + end + + def cast(value) + return nil if value.nil? + return value if value.is_a?(@model_class) + return @model_class.new(value) if value.is_a?(::Hash) + + raise ArgumentError.new("Nested: #{value.inspect} (#{value.class}) is not supported for cast") + end + + def serialize(value) + return nil if value.nil? + + value = @model_class.new(value) if value.is_a?(::Hash) + return value.send(:serialized_attributes) if value.is_a?(@model_class) + + raise ArgumentError.new("Nested: #{value.inspect} (#{value.class}) is not supported for serialization") + end end + end end diff --git a/lib/couchbase-orm/types/timestamp.rb b/lib/couchbase-orm/types/timestamp.rb index d3643e87..c0de3879 100644 --- a/lib/couchbase-orm/types/timestamp.rb +++ b/lib/couchbase-orm/types/timestamp.rb @@ -1,18 +1,20 @@ +# frozen_string_literal: true + 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) || value.is_a?(Float) - 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 + module Types + class Timestamp < ActiveModel::Type::DateTime + def cast(value) + return nil if value.nil? + return Time.at(value) if value.is_a?(Integer) || value.is_a?(Float) + 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/ensure_unique.rb b/lib/couchbase-orm/utilities/ensure_unique.rb index 0da3da49..14c22781 100644 --- a/lib/couchbase-orm/utilities/ensure_unique.rb +++ b/lib/couchbase-orm/utilities/ensure_unique.rb @@ -1,18 +1,20 @@ +# frozen_string_literal: true + module CouchbaseOrm - module EnsureUnique - private + module EnsureUnique + private - def ensure_unique(attrs, name = nil, presence: true, &processor) - # index uses a special bucket key to allow record lookups based on - # the values of attrs. ensure_unique adds a simple lookup using - # one of the added methods to identify duplicate - name = index(attrs, name, presence: presence, &processor) + def ensure_unique(attrs, name = nil, presence: true, &processor) + # index uses a special bucket key to allow record lookups based on + # the values of attrs. ensure_unique adds a simple lookup using + # one of the added methods to identify duplicate + name = index(attrs, name, presence: presence, &processor) - validate do |record| - unless record.send("#{name}_unique?") - errors.add(name, 'has already been taken') - end - end + validate do |record| + unless record.send("#{name}_unique?") + errors.add(name, 'has already been taken') end + end end + end end diff --git a/lib/couchbase-orm/utilities/enum.rb b/lib/couchbase-orm/utilities/enum.rb index 6032f90c..10310cf3 100644 --- a/lib/couchbase-orm/utilities/enum.rb +++ b/lib/couchbase-orm/utilities/enum.rb @@ -1,61 +1,63 @@ +# frozen_string_literal: true + module CouchbaseOrm - module Enum - private - - def enum(options) - # options contains an optional default value, and the name of the - # enum, e.g enum visibility: %i(group org public), default: :group - default = options.delete(:default) - name = options.keys.first.to_sym - values = options[name] - - # values is assumed to be a list of symbols. each value is assigned an - # integer, and this number is used for db storage. numbers start at 1. - mapping = {} - values.each_with_index do |value, i| - mapping[value.to_sym] = i + 1 - mapping[i + 1] = value.to_sym - end - - # VISIBILITY = {group: 0, 0: group ...} - const_set(name.to_s.upcase, mapping) - - # lookup the default's integer value - if default - default_value = mapping[default] - raise 'Unknown default value' unless default_value - else - default_value = 1 - end - 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| - value = record[name] - - unless value.nil? - value = case value - when Symbol, String - record.class.const_get(name.to_s.upcase)[value.to_sym] - else - Integer(value) - end - end - - record[name] = (1..values.length).cover?(value) ? value : default_value - end + module Enum + private + + def enum(options) + # options contains an optional default value, and the name of the + # enum, e.g enum visibility: %i(group org public), default: :group + default = options.delete(:default) + name = options.keys.first.to_sym + values = options[name] + + # values is assumed to be a list of symbols. each value is assigned an + # integer, and this number is used for db storage. numbers start at 1. + mapping = {} + values.each_with_index do |value, i| + mapping[value.to_sym] = i + 1 + mapping[i + 1] = value.to_sym + end + + # VISIBILITY = {group: 0, 0: group ...} + const_set(name.to_s.upcase, mapping) + + # lookup the default's integer value + if default + default_value = mapping[default] + raise 'Unknown default value' unless default_value + else + default_value = 1 + end + 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| + value = record[name] + + unless value.nil? + value = case value + when Symbol, String + record.class.const_get(name.to_s.upcase)[value.to_sym] + else + Integer(value) + end end + + record[name] = (1..values.length).cover?(value) ? value : default_value + end end + end end diff --git a/lib/couchbase-orm/utilities/has_many.rb b/lib/couchbase-orm/utilities/has_many.rb index ce14d003..b5ffda70 100644 --- a/lib/couchbase-orm/utilities/has_many.rb +++ b/lib/couchbase-orm/utilities/has_many.rb @@ -1,113 +1,119 @@ +# frozen_string_literal: true + module CouchbaseOrm - module HasMany - # :foreign_key, :class_name, :through - def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_class: nil, through_key: nil, type: :view, **options) - class_name = (class_name || model.to_s.singularize.camelcase).to_s - foreign_key = (foreign_key || ActiveSupport::Inflector.foreign_key(self.name)).to_sym - if through || through_class - remote_class = class_name - class_name = (through_class || through.to_s.camelcase).to_s - through_key = (through_key || "#{remote_class.underscore}_id").to_sym - remote_method = :"by_#{foreign_key}_with_#{through_key}" - else - remote_method = :"find_by_#{foreign_key}" - end + module HasMany + # :foreign_key, :class_name, :through + def has_many(model, class_name: nil, foreign_key: nil, through: nil, through_class: nil, through_key: nil, + type: :view, **options) + class_name = (class_name || model.to_s.singularize.camelcase).to_s + foreign_key = (foreign_key || ActiveSupport::Inflector.foreign_key(self.name)).to_sym + if through || through_class + remote_class = class_name + class_name = (through_class || through.to_s.camelcase).to_s + through_key = (through_key || "#{remote_class.underscore}_id").to_sym + remote_method = :"by_#{foreign_key}_with_#{through_key}" + else + remote_method = :"find_by_#{foreign_key}" + end - instance_var = "@__assoc_#{model}" + instance_var = "@__assoc_#{model}" - klass = begin - class_name.constantize - rescue NameError - warn "WARNING: #{class_name} referenced in #{self.name} before it was aded" + klass = begin + class_name.constantize + 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 - Object.class_eval <<-EKLASS + # 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 + Object.class_eval <<-EKLASS, __FILE__, __LINE__ + 1 class #{class_name} < CouchbaseOrm::Base attribute :#{foreign_key} end - EKLASS - class_name.constantize - end - - build_index(type, klass, remote_class, remote_method, through_key, foreign_key) - - if remote_class - define_method(model) do - 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 - remote_klass.find(row) - when :view - remote_klass.find(row[through_key]) - else - raise 'type is unknown' - end - } - - self.instance_variable_set(instance_var, enum) - end - else - define_method(model) do - return self.instance_variable_get(instance_var) if instance_variable_defined?(instance_var) - self.instance_variable_set(instance_var, self.id ? klass.__send__(remote_method, self.id) : []) - end - end + EKLASS + class_name.constantize + end - define_method(:"#{model}_reset") do - self.remove_instance_variable(instance_var) if self.instance_variable_defined?(instance_var) - end + build_index(type, klass, remote_class, remote_method, through_key, foreign_key) - @associations ||= [] - @associations << [model, options[:dependent]] - end + if remote_class + define_method(model) do + return self.instance_variable_get(instance_var) if instance_variable_defined?(instance_var) + + remote_klass = remote_class.constantize + raise ArgumentError.new("Can't find #{remote_method} without an id") unless self.id.present? - def build_index(type, klass, remote_class, remote_method, through_key, foreign_key) + enum = klass.__send__(remote_method, key: self.id) { |row| case type when :n1ql - build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key) + remote_klass.find(row) when :view - build_index_view(klass, remote_class, remote_method, through_key, foreign_key) + remote_klass.find(row[through_key]) else - raise 'type is unknown' + raise 'type is unknown' end + } + + self.instance_variable_set(instance_var, enum) end + else + define_method(model) do + return self.instance_variable_get(instance_var) if instance_variable_defined?(instance_var) + + self.instance_variable_set(instance_var, self.id ? klass.__send__(remote_method, self.id) : []) + end + end + + define_method(:"#{model}_reset") do + self.remove_instance_variable(instance_var) if self.instance_variable_defined?(instance_var) + end - def build_index_view(klass, remote_class, remote_method, through_key, foreign_key) - if remote_class - klass.class_eval do - view remote_method, map: <<-EMAP + @associations ||= [] + @associations << [model, options[:dependent]] + end + + def build_index(type, klass, remote_class, remote_method, through_key, foreign_key) + case type + when :n1ql + build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key) + when :view + build_index_view(klass, remote_class, remote_method, through_key, foreign_key) + else + raise 'type is unknown' + end + end + + def build_index_view(klass, remote_class, remote_method, through_key, foreign_key) + if remote_class + klass.class_eval do + view remote_method, map: <<-EMAP function(doc) { if (doc.type === "{{design_document}}" && doc.#{through_key}) { emit(doc.#{foreign_key}, null); } } - EMAP - end - else - klass.class_eval do - index_view foreign_key, validate: false - end - end + EMAP end + else + klass.class_eval do + index_view foreign_key, validate: false + end + end + end - def build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key) - if remote_class - klass.class_eval do - 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} = #{quote(values[0])}", options) - } - end - else - klass.class_eval do - index_n1ql foreign_key, validate: false - end - end + def build_index_n1ql(klass, remote_class, remote_method, through_key, foreign_key) + if remote_class + klass.class_eval do + n1ql remote_method, emit_key: 'id', query_fn: proc { |bucket, values, options| + raise ArgumentError.new('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} = #{quote(values[0])}", options) + } + end + else + klass.class_eval do + index_n1ql foreign_key, validate: false end + end end + end end diff --git a/lib/couchbase-orm/utilities/ignored_properties.rb b/lib/couchbase-orm/utilities/ignored_properties.rb index 7ef5cfd9..e5d4b283 100644 --- a/lib/couchbase-orm/utilities/ignored_properties.rb +++ b/lib/couchbase-orm/utilities/ignored_properties.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + module CouchbaseOrm - module IgnoredProperties - def ignored_properties(*args) - @@ignored_properties ||= [] - return @@ignored_properties if args.empty? - @@ignored_properties += args.map(&:to_s) - end + module IgnoredProperties + def ignored_properties(*args) + @@ignored_properties ||= [] + return @@ignored_properties if args.empty? + + @@ignored_properties += args.map(&:to_s) end + end end diff --git a/lib/couchbase-orm/utilities/index.rb b/lib/couchbase-orm/utilities/index.rb index bec602f2..77d54953 100644 --- a/lib/couchbase-orm/utilities/index.rb +++ b/lib/couchbase-orm/utilities/index.rb @@ -1,137 +1,138 @@ -module CouchbaseOrm - module Index - private - - def index(attrs, name = nil, presence: true, &processor) - attrs = Array(attrs).flatten - name ||= attrs.map(&:to_s).join('_') - - find_by_method = "find_by_#{name}" - processor_method = "process_#{name}" - bucket_key_method = "#{name}_bucket_key" - bucket_key_vals_method = "#{name}_bucket_key_vals" - class_bucket_key_method = "generate_#{bucket_key_method}" - original_bucket_key_var = "@original_#{bucket_key_method}" - - - #---------------- - # keys - #---------------- - # class method to generate a bucket key given input values - define_singleton_method(class_bucket_key_method) do |*values| - processed = self.send(processor_method, *values) - "#{@design_document}#{name}-#{processed}" - end - - # instance method that uses the class method to generate a bucket key - # given the current value of each of the key's component attributes - define_method(bucket_key_method) do |args = nil| - self.class.send(class_bucket_key_method, *self.send(bucket_key_vals_method)) - end - - # collect a list of values for each key component attribute - define_method(bucket_key_vals_method) do - attrs.collect {|attr| self.class.attribute_types[attr.to_s].cast(self[attr])} - end - - - #---------------- - # helpers - #---------------- - # 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 - values.join('-') - end - end - - # 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) - return mod if mod - - # 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 - end - - - #---------------- - # validations - #---------------- - # ensure each component of the unique key is present - if presence - attrs.each do |attr| - validates attr, presence: true - attribute attr - end - end - - define_method("#{name}_unique?") do - values = self.send(bucket_key_vals_method) - other = self.class.send(find_by_method, *values) - !other || other.id == self.id - end +# frozen_string_literal: true +module CouchbaseOrm + module Index + private + + def index(attrs, name = nil, presence: true, &processor) + attrs = Array(attrs).flatten + name ||= attrs.map(&:to_s).join('_') + + find_by_method = "find_by_#{name}" + processor_method = "process_#{name}" + bucket_key_method = "#{name}_bucket_key" + bucket_key_vals_method = "#{name}_bucket_key_vals" + class_bucket_key_method = "generate_#{bucket_key_method}" + original_bucket_key_var = "@original_#{bucket_key_method}" + + #---------------- + # keys + #---------------- + # class method to generate a bucket key given input values + define_singleton_method(class_bucket_key_method) do |*values| + processed = self.send(processor_method, *values) + "#{@design_document}#{name}-#{processed}" + end + + # instance method that uses the class method to generate a bucket key + # given the current value of each of the key's component attributes + define_method(bucket_key_method) do |_args = nil| + self.class.send(class_bucket_key_method, *self.send(bucket_key_vals_method)) + end + + # collect a list of values for each key component attribute + define_method(bucket_key_vals_method) do + attrs.collect { |attr| self.class.attribute_types[attr.to_s].cast(self[attr]) } + end + + #---------------- + # helpers + #---------------- + # 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 + yield(values.length == 1 ? values.first : values) + else + values.join('-') + end + end + + # 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) + return mod if mod + + # Clean up record if the id doesn't exist + self.collection.remove(key) + else + CouchbaseOrm.logger.debug("#{find_by_method}: #{key} not found") + end - #---------------- - # callbacks - #---------------- - # before a save is complete, while changes are still available, store - # a copy of the current bucket key for comparison if any of the key - # components have been modified - before_save do |record| - if attrs.any? { |attr| record.changes.include?(attr) } - args = attrs.collect { |attr| send(:"#{attr}_was") || send(attr) } - instance_variable_set(original_bucket_key_var, self.class.send(class_bucket_key_method, *args)) - end + nil + end + + #---------------- + # validations + #---------------- + # ensure each component of the unique key is present + if presence + attrs.each do |attr| + validates attr, presence: true + attribute attr + end + end + + define_method("#{name}_unique?") do + values = self.send(bucket_key_vals_method) + other = self.class.send(find_by_method, *values) + !other || other.id == self.id + end + + #---------------- + # callbacks + #---------------- + # before a save is complete, while changes are still available, store + # a copy of the current bucket key for comparison if any of the key + # components have been modified + before_save do |record| + if attrs.any? { |attr| record.changes.include?(attr) } + args = attrs.collect { |attr| send(:"#{attr}_was") || send(attr) } + instance_variable_set(original_bucket_key_var, self.class.send(class_bucket_key_method, *args)) + end + end + + # after the values are persisted, delete the previous key and store the + # new one. the id of the current record is used as the key's value. + after_save do |record| + original_key = instance_variable_get(original_bucket_key_var) + + if original_key + 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 - # after the values are persisted, delete the previous key and store the - # new one. the id of the current record is used as the key's value. - after_save do |record| - original_key = instance_variable_get(original_bucket_key_var) - - if original_key - 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 - - record.class.collection.upsert(record.send(bucket_key_method), record.id) - - instance_variable_set(original_bucket_key_var, nil) - end + record.class.collection.upsert(record.send(bucket_key_method), record.id) - # 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.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 + instance_variable_set(original_bucket_key_var, nil) + end - # return the name used to construct the added method names so other - # code can call the special index methods easily - return name + # 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.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 + # return the name used to construct the added method names so other + # code can call the special index methods easily + name end + end end diff --git a/lib/couchbase-orm/utilities/join.rb b/lib/couchbase-orm/utilities/join.rb index 3dc74ec8..6930bb33 100644 --- a/lib/couchbase-orm/utilities/join.rb +++ b/lib/couchbase-orm/utilities/join.rb @@ -1,68 +1,70 @@ +# frozen_string_literal: true + module CouchbaseOrm - module Join - private + module Join + private - # join adds methods for retrieving the join model by user or group, and - # methods for retrieving either model through the join model (e.g all - # users who are in a group). model_a and model_b must be strings or symbols - # and are assumed to be singularised, underscored versions of model names - def join(model_a, model_b, options={}) - # store the join model names for use by has_many associations - @join_models = [model_a.to_s, model_b.to_s] + # join adds methods for retrieving the join model by user or group, and + # methods for retrieving either model through the join model (e.g all + # users who are in a group). model_a and model_b must be strings or symbols + # and are assumed to be singularised, underscored versions of model names + def join(model_a, model_b, options = {}) + # store the join model names for use by has_many associations + @join_models = [model_a.to_s, model_b.to_s] - # join :user, :group => design_document :ugj - doc_name = options[:design_document] || "#{model_a.to_s[0]}#{model_b.to_s[0]}j".to_sym - design_document doc_name + # join :user, :group => design_document :ugj + doc_name = options[:design_document] || "#{model_a.to_s[0]}#{model_b.to_s[0]}j".to_sym + design_document doc_name - # a => b - add_single_sided_features(model_a) - add_joint_lookups(model_a, model_b) + # a => b + add_single_sided_features(model_a) + add_joint_lookups(model_a, model_b) - # b => a - add_single_sided_features(model_b) - add_joint_lookups(model_b, model_a, true) + # b => a + add_single_sided_features(model_b) + add_joint_lookups(model_b, model_a, true) - # use Index to allow lookups of joint records more efficiently than - # with a view or search - index ["#{model_a}_id".to_sym, "#{model_b}_id".to_sym], :join - end + # use Index to allow lookups of joint records more efficiently than + # with a view or search + index ["#{model_a}_id".to_sym, "#{model_b}_id".to_sym], :join + end - def add_single_sided_features(model) - # belongs_to :group - belongs_to model + def add_single_sided_features(model) + # belongs_to :group + belongs_to model - # view :by_group_id - view "by_#{model}_id" + # view :by_group_id + view "by_#{model}_id" - # find_by_group_id - instance_eval " + # find_by_group_id + instance_eval " def self.find_by_#{model}_id(#{model}_id) by_#{model}_id(key: #{model}_id) end - " - end + ", __FILE__, __LINE__ - 4 + end - def add_joint_lookups(model_a, model_b, reverse = false) - # find_by_user_id_and_group_id - instance_eval " + def add_joint_lookups(model_a, model_b, reverse = false) + # find_by_user_id_and_group_id + instance_eval " def self.find_by_#{model_a}_id_and_#{model_b}_id(#{model_a}_id, #{model_b}_id) self.find_by_join([#{reverse ? model_b : model_a}_id, #{reverse ? model_a : model_b}_id]) end - " + ", __FILE__, __LINE__ - 4 - # user_ids_by_group_id - instance_eval " + # user_ids_by_group_id + instance_eval " def self.#{model_a}_ids_by_#{model_b}_id(#{model_b}_id) self.find_by_#{model_b}_id(#{model_b}_id).map(&:#{model_a}_id) end - " + ", __FILE__, __LINE__ - 4 - # users_by_group_id - instance_eval " + # users_by_group_id + instance_eval " def self.#{model_a.to_s.pluralize}_by_#{model_b}_id(#{model_b}_id) self.find_by_#{model_b}_id(#{model_b}_id).map(&:#{model_a}) end - " - end + ", __FILE__, __LINE__ - 4 end + end end diff --git a/lib/couchbase-orm/utilities/query_helper.rb b/lib/couchbase-orm/utilities/query_helper.rb index c51af322..9bfbe152 100644 --- a/lib/couchbase-orm/utilities/query_helper.rb +++ b/lib/couchbase-orm/utilities/query_helper.rb @@ -1,128 +1,125 @@ -module CouchbaseOrm - module QueryHelper - extend ActiveSupport::Concern +# frozen_string_literal: true - module ClassMethods +module CouchbaseOrm + module QueryHelper + extend ActiveSupport::Concern - def build_match(key, value) - key = "meta().id" if key.to_s == "id" - case - when value.nil? - "#{key} IS NOT VALUED" - when value.is_a?(Hash) - build_match_hash(key, value) - when value.is_a?(Array) && value.include?(nil) - "(#{build_match(key, nil)} OR #{build_match(key, value.compact)})" - when value.is_a?(Array) - "#{key} IN #{quote(value)}" - else - "#{key} = #{quote(value)}" - end - end + module ClassMethods + def build_match(key, value) + key = 'meta().id' if key.to_s == 'id' + if value.nil? + "#{key} IS NOT VALUED" + elsif value.is_a?(Hash) + build_match_hash(key, value) + elsif value.is_a?(Array) && value.include?(nil) + "(#{build_match(key, nil)} OR #{build_match(key, value.compact)})" + elsif value.is_a?(Array) + "#{key} IN #{quote(value)}" + else + "#{key} = #{quote(value)}" + end + end - def build_match_hash(key, value) - matches = [] - value.each do |k, v| - case k - when :_gt - matches << "#{key} > #{quote(v)}" - when :_gte - matches << "#{key} >= #{quote(v)}" - when :_lt - matches << "#{key} < #{quote(v)}" - when :_lte - matches << "#{key} <= #{quote(v)}" - when :_ne - matches << "#{key} != #{quote(v)}" - - # TODO v2 - # when :_in - # matches << "#{key} IN #{quote(v)}" - # when :_nin - # matches << "#{key} NOT IN #{quote(v)}" - # when :_like - # matches << "#{key} LIKE #{quote(v)}" - # when :_nlike - # matches << "#{key} NOT LIKE #{quote(v)}" - # when :_between - # matches << "#{key} BETWEEN #{quote(v[0])} AND #{quote(v[1])}" - # when :_nbetween - # matches << "#{key} NOT BETWEEN #{quote(v[0])} AND #{quote(v[1])}" - # when :_exists - # matches << "#{key} IS #{v ? "" : "NOT "}VALUED" - # when :_regex - # matches << "#{key} REGEXP #{quote(v)}" - # when :_nregex - # matches << "#{key} NOT REGEXP #{quote(v)}" - # when :_match - # matches << "#{key} MATCH #{quote(v)}" - # when :_nmatch - # matches << "#{key} NOT MATCH #{quote(v)}" - - # TODO v3 - # when :_any - # matches << "#{key} ANY #{quote(v)}" - # when :_nany - # matches << "#{key} NOT ANY #{quote(v)}" - # when :_all - # matches << "#{key} ALL #{quote(v)}" - # when :_nall - # matches << "#{key} NOT ALL #{quote(v)}" - # when :_within - # matches << "#{key} WITHIN #{quote(v)}" - #when :_nwithin - # matches << "#{key} NOT WITHIN #{quote(v)}" - else - if attribute_types[key.to_s].is_a?(CouchbaseOrm::Types::Array) - matches << "any #{key.to_s.singularize} in #{key} satisfies #{build_match("#{key.to_s.singularize}.#{k}", v)} end" - else - matches << build_match("#{key}.#{k}", v) - end - end - end - - matches.join(" AND ") - end + def build_match_hash(key, value) + value.map do |k, v| + case k + when :_gt + "#{key} > #{quote(v)}" + when :_gte + "#{key} >= #{quote(v)}" + when :_lt + "#{key} < #{quote(v)}" + when :_lte + "#{key} <= #{quote(v)}" + when :_ne + "#{key} != #{quote(v)}" + # TODO: v2 + # when :_in + # "#{key} IN #{quote(v)}" + # when :_nin + # "#{key} NOT IN #{quote(v)}" + # when :_like + # "#{key} LIKE #{quote(v)}" + # when :_nlike + # "#{key} NOT LIKE #{quote(v)}" + # when :_between + # "#{key} BETWEEN #{quote(v[0])} AND #{quote(v[1])}" + # when :_nbetween + # "#{key} NOT BETWEEN #{quote(v[0])} AND #{quote(v[1])}" + # when :_exists + # "#{key} IS #{v ? "" : "NOT "}VALUED" + # when :_regex + # "#{key} REGEXP #{quote(v)}" + # when :_nregex + # "#{key} NOT REGEXP #{quote(v)}" + # when :_match + # "#{key} MATCH #{quote(v)}" + # when :_nmatch + # "#{key} NOT MATCH #{quote(v)}" - def build_not_match(key, value) - key = "meta().id" if key.to_s == "id" - case - when value.nil? - "#{key} IS VALUED" - when value.is_a?(Array) && value.include?(nil) - "(#{build_not_match(key, nil)} AND #{build_not_match(key, value.compact)})" - when value.is_a?(Array) - "#{key} NOT IN #{quote(value)}" - else - "#{key} != #{quote(value)}" - end + # TODO: v3 + # when :_any + # "#{key} ANY #{quote(v)}" + # when :_nany + # "#{key} NOT ANY #{quote(v)}" + # when :_all + # "#{key} ALL #{quote(v)}" + # when :_nall + # "#{key} NOT ALL #{quote(v)}" + # when :_within + # "#{key} WITHIN #{quote(v)}" + # when :_nwithin + # "#{key} NOT WITHIN #{quote(v)}" + else + if attribute_types[key.to_s].is_a?(CouchbaseOrm::Types::Array) + "any #{key.to_s.singularize} in #{key} satisfies #{build_match("#{key.to_s.singularize}.#{k}", v)} end" + else + build_match("#{key}.#{k}", v) end + end + end.join(' AND ') + end - def serialize_value(key, value_before_type_cast) - 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]}" } - value - end + def build_not_match(key, value) + key = 'meta().id' if key.to_s == 'id' + if value.nil? + "#{key} IS VALUED" + elsif value.is_a?(Array) && value.include?(nil) + "(#{build_not_match(key, nil)} AND #{build_not_match(key, value.compact)})" + elsif value.is_a?(Array) + "#{key} NOT IN #{quote(value)}" + else + "#{key} != #{quote(value)}" + end + end - def quote(value) - if value.is_a? String - "'#{N1ql.sanitize(value)}'" - elsif value.is_a? Array - "[#{value.map{|v|quote(v)}.join(', ')}]" - elsif value.nil? - nil - else - N1ql.sanitize(value).to_s - end + def serialize_value(key, value_before_type_cast) + 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]}" + } + value + end + + def quote(value) + if value.is_a? String + "'#{N1ql.sanitize(value)}'" + elsif value.is_a? Array + "[#{value.map{ |v| quote(v) }.join(', ')}]" + elsif value.nil? + nil + else + N1ql.sanitize(value).to_s end + end end + end end diff --git a/lib/couchbase-orm/version.rb b/lib/couchbase-orm/version.rb index 189be47d..3ec2e8fe 100644 --- a/lib/couchbase-orm/version.rb +++ b/lib/couchbase-orm/version.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true module CouchbaseOrm - VERSION = '1.2.0' + VERSION = '1.2.0' end diff --git a/lib/couchbase-orm/views.rb b/lib/couchbase-orm/views.rb index b2e55493..8a2aed4f 100644 --- a/lib/couchbase-orm/views.rb +++ b/lib/couchbase-orm/views.rb @@ -1,161 +1,164 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true require 'active_model' module CouchbaseOrm - module Views - extend ActiveSupport::Concern - - - module ClassMethods - # Defines a view for the model - # - # @param [Symbol, String, Array] names names of the views - # @param [Hash] options options passed to the {Couchbase::View} - # - # @example Define some views for a model - # class Post < CouchbaseOrm::Base - # view :all - # view :by_rating, emit_key: :rating - # end - # - # Post.by_rating do |response| - # # ... - # end - def view(name, map: nil, emit_key: nil, reduce: nil, **options) - raise ArgumentError, "#{self} already respond_to? #{name}" if self.respond_to?(name) - - if emit_key.class == Array - emit_key.each do |key| - 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 && !attribute_names.include?(emit_key.to_s) - end - - options = ViewDefaults.merge(options) - - method_opts = {} - method_opts[:map] = map if map - method_opts[:reduce] = reduce if reduce - - unless method_opts.has_key? :map - if emit_key.class == Array - method_opts[:map] = <<-EMAP -function(doc) { - if (doc.type === "{{design_document}}") { - emit([#{emit_key.map{|key| "doc."+key.to_s}.join(',')}], null); - } -} -EMAP - else - emit_key = emit_key || :created_at - method_opts[:map] = <<-EMAP -function(doc) { - if (doc.type === "{{design_document}}") { - emit(doc.#{emit_key}, null); - } -} -EMAP - end - end - - @views ||= {} - - name = name.to_sym - @views[name] = method_opts - - singleton_class.__send__(:define_method, name) do |**opts, &result_modifier| - opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus) - CouchbaseOrm.logger.debug("View [#{@design_document}, #{name.inspect}] options: #{opts.inspect}") - if 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] - include_docs(bucket.view_query(@design_document, name.to_s, Couchbase::Options::View.new(**opts.except(:include_docs)))) - else - bucket.view_query(@design_document, name.to_s, Couchbase::Options::View.new(**opts.except(:include_docs))) - end - end - end - ViewDefaults = {include_docs: true} + module Views + extend ActiveSupport::Concern + + module ClassMethods + # Defines a view for the model + # + # @param [Symbol, String, Array] names names of the views + # @param [Hash] options options passed to the {Couchbase::View} + # + # @example Define some views for a model + # class Post < CouchbaseOrm::Base + # view :all + # view :by_rating, emit_key: :rating + # end + # + # Post.by_rating do |response| + # # ... + # end + def view(name, map: nil, emit_key: nil, reduce: nil, **options) + raise ArgumentError.new("#{self} already respond_to? #{name}") if self.respond_to?(name) + + if emit_key.is_a?(Array) + emit_key.each do |key| + raise "unknown emit_key attribute for view :#{name}, emit_key: :#{key}" if key && !attribute_names.include?(key.to_s) + end + elsif emit_key && !attribute_names.include?(emit_key.to_s) + raise "unknown emit_key attribute for view :#{name}, emit_key: :#{emit_key}" + end - # add a view and lookup method to the model for finding all records - # using a value in the supplied attr. - def index_view(attr, validate: true, find_method: nil, view_method: nil) - view_method ||= "by_#{attr}" - find_method ||= "find_#{view_method}" + options = ViewDefaults.merge(options) + + method_opts = {} + method_opts[:map] = map if map + method_opts[:reduce] = reduce if reduce + + unless method_opts.key? :map + if emit_key.instance_of?(Array) + method_opts[:map] = <<~EMAP + function(doc) { + if (doc.type === "{{design_document}}") { + emit([#{emit_key.map{ |key| 'doc.' + key.to_s }.join(',')}], null); + } + } + EMAP + else + emit_key ||= :created_at + method_opts[:map] = <<~EMAP + function(doc) { + if (doc.type === "{{design_document}}") { + emit(doc.#{emit_key}, null); + } + } + EMAP + end + end + + @views ||= {} + + name = name.to_sym + @views[name] = method_opts + + singleton_class.__send__(:define_method, name) do |**opts, &result_modifier| + opts = options.merge(opts).reverse_merge(scan_consistency: :request_plus) + CouchbaseOrm.logger.debug("View [#{@design_document}, #{name.inspect}] options: #{opts.inspect}") + if 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] + include_docs(bucket.view_query(@design_document, name.to_s, + Couchbase::Options::View.new(**opts.except(:include_docs)))) + else + bucket.view_query(@design_document, name.to_s, Couchbase::Options::View.new(**opts.except(:include_docs))) + end + end + end + ViewDefaults = {include_docs: true}.freeze - validates(attr, presence: true) if validate - view view_method, emit_key: attr + # add a view and lookup method to the model for finding all records + # using a value in the supplied attr. + def index_view(attr, validate: true, find_method: nil, view_method: nil) + view_method ||= "by_#{attr}" + find_method ||= "find_#{view_method}" - instance_eval " + validates(attr, presence: true) if validate + view view_method, emit_key: attr + + instance_eval " def self.#{find_method}(#{attr}) #{view_method}(key: #{attr}) end - " - end + ", __FILE__, __LINE__ - 4 + end - def ensure_design_document! - return false unless @views && !@views.empty? - existing = {} - update_required = false - - # Grab the existing view details - 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| - 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+/, '') - - unless cmap == dmap && creduce == dreduce - update_required = true - break - end - else - update_required = true - break - end - end - - # Updated the design document - if update_required - document = Couchbase::Management::DesignDocument.new - document.views = views_actual - document.name = @design_document - bucket.view_indexes.upsert_design_document(document, :production) - - true - else - false - end - end + def ensure_design_document! + return false unless @views && !@views.empty? + + existing = {} + update_required = false + + # Grab the existing view details + 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| + views_actual[name.to_s] = Couchbase::Management::View.new( + document[:map]&.gsub('{{design_document}}', @design_document), + document[:reduce]&.gsub('{{design_document}}', @design_document) + ) + 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 + # 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+/, '') + + unless cmap == dmap && creduce == dreduce + update_required = true + break end + else + update_required = true + break + end + end + + # Updated the design document + if update_required + document = Couchbase::Management::DesignDocument.new + document.views = views_actual + document.name = @design_document + bucket.view_indexes.upsert_design_document(document, :production) + + 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/ext/query_n1ql.rb b/lib/ext/query_n1ql.rb index 1d1bd0cd..d822cbb3 100644 --- a/lib/ext/query_n1ql.rb +++ b/lib/ext/query_n1ql.rb @@ -1,124 +1,125 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true module MTLibcouchbase - class QueryN1QL - N1P_QUERY_STATEMENT = 1 - N1P_CONSISTENCY_REQUEST = 2 + class QueryN1QL + N1P_QUERY_STATEMENT = 1 + N1P_CONSISTENCY_REQUEST = 2 - def initialize(connection, reactor, n1ql, **_opts) - @connection = connection - @reactor = reactor + def initialize(connection, reactor, n1ql, **_opts) + @connection = connection + @reactor = reactor - @n1ql = n1ql - @request_handle = FFI::MemoryPointer.new :pointer, 1 - end + @n1ql = n1ql + @request_handle = FFI::MemoryPointer.new :pointer, 1 + end - attr_reader :connection, :n1ql + attr_reader :connection, :n1ql - def get_count(metadata) - metadata[:metrics][:resultCount] - end + def get_count(metadata) + metadata[:metrics][:resultCount] + end - def perform(limit: nil, **_options, &blk) - raise 'not connected' unless @connection.handle - raise 'query already in progress' if @query_text - raise 'callback required' unless block_given? - - # customise the size based on the request being made - orig_limit = @n1ql.limit - begin - if orig_limit && limit - @n1ql.limit = limit if orig_limit > limit - end - @query_text = @n1ql.to_s - rescue StandardError - @query_text = nil - raise - ensure - @n1ql.limit = orig_limit - end + def perform(limit: nil, **_options, &blk) + raise 'not connected' unless @connection.handle + raise 'query already in progress' if @query_text + raise 'callback required' unless block_given? - @reactor.schedule do - @error = nil - @callback = blk - - @cmd = Ext::CMDN1QL.new - @params = Ext.n1p_new - err = Ext.n1p_setconsistency(@params, N1P_CONSISTENCY_REQUEST) - if err == :success - err = Ext.n1p_setquery(@params, @query_text, @query_text.bytesize, N1P_QUERY_STATEMENT) - if err == :success - - err = Ext.n1p_mkcmd(@params, @cmd) - if err == :success - pointer = @cmd.to_ptr - @connection.requests[pointer.address] = self - - @cmd[:callback] = @connection.get_callback(:n1ql_callback) - @cmd[:handle] = @request_handle - - err = Ext.n1ql_query(@connection.handle, pointer, @cmd) - if err != :success - error(Error.lookup(err).new('full text search not scheduled')) - end - else - error(Error.lookup(err).new('failed to build full text search command')) - end - else - error(Error.lookup(err).new('failed to build full text search query structure')) - end - else - error(Error.lookup(err).new('failed set consistency value')) - end + # customise the size based on the request being made + orig_limit = @n1ql.limit + begin + if orig_limit && limit && (orig_limit > limit) + @n1ql.limit = limit + end + @query_text = @n1ql.to_s + rescue StandardError + @query_text = nil + raise + ensure + @n1ql.limit = orig_limit + end + + @reactor.schedule do + @error = nil + @callback = blk + + @cmd = Ext::CMDN1QL.new + @params = Ext.n1p_new + err = Ext.n1p_setconsistency(@params, N1P_CONSISTENCY_REQUEST) + if err == :success + err = Ext.n1p_setquery(@params, @query_text, @query_text.bytesize, N1P_QUERY_STATEMENT) + if err == :success + + err = Ext.n1p_mkcmd(@params, @cmd) + if err == :success + pointer = @cmd.to_ptr + @connection.requests[pointer.address] = self + + @cmd[:callback] = @connection.get_callback(:n1ql_callback) + @cmd[:handle] = @request_handle + + err = Ext.n1ql_query(@connection.handle, pointer, @cmd) + if err != :success + error(Error.lookup(err).new('full text search not scheduled')) + end + else + error(Error.lookup(err).new('failed to build full text search command')) end + else + error(Error.lookup(err).new('failed to build full text search query structure')) + end + else + error(Error.lookup(err).new('failed set consistency value')) end + end + end - # Row is JSON value representing the result - def received(row) - return if @error + # Row is JSON value representing the result + def received(row) + return if @error - @callback.call(false, row) - rescue StandardError => e - @error = e - cancel - end + @callback.call(false, row) + rescue StandardError => e + @error = e + cancel + end - # Example metadata - # {:requestID=>"36162fce-ef39-4821-bf03-449e4073185d", :signature=>{:*=>"*"}, :results=>[], :status=>"success", - # :metrics=>{:elapsedTime=>"15.298243ms", :executionTime=>"15.256975ms", :resultCount=>12, :resultSize=>8964}} - def received_final(metadata) - @query_text = nil + # Example metadata + # {:requestID=>"36162fce-ef39-4821-bf03-449e4073185d", :signature=>{:*=>"*"}, :results=>[], :status=>"success", + # :metrics=>{:elapsedTime=>"15.298243ms", :executionTime=>"15.256975ms", :resultCount=>12, :resultSize=>8964}} + def received_final(metadata) + @query_text = nil - @connection.requests.delete(@cmd.to_ptr.address) - @cmd = nil + @connection.requests.delete(@cmd.to_ptr.address) + @cmd = nil - Ext.n1p_free(@params) - @params = nil + Ext.n1p_free(@params) + @params = nil - if @error - if @error == :cancelled - @callback.call(:final, metadata) - else - @callback.call(:error, @error) - end - else - @callback.call(:final, metadata) - end + if @error + if @error == :cancelled + @callback.call(:final, metadata) + else + @callback.call(:error, @error) end + else + @callback.call(:final, metadata) + end + end - def error(obj) - @error = obj - received_final(nil) - end + def error(obj) + @error = obj + received_final(nil) + end - def cancel - @error ||= :cancelled - @reactor.schedule do - if @connection.handle && @cmd - Ext.n1ql_cancel(@connection.handle, @handle_ptr.get_pointer(0)) - received_final(nil) - end - end + def cancel + @error ||= :cancelled + @reactor.schedule do + if @connection.handle && @cmd + Ext.n1ql_cancel(@connection.handle, @handle_ptr.get_pointer(0)) + received_final(nil) end + end end + end end diff --git a/lib/rails/generators/couchbase_orm/config/config_generator.rb b/lib/rails/generators/couchbase_orm/config/config_generator.rb index 2e6e4819..5b871052 100644 --- a/lib/rails/generators/couchbase_orm/config/config_generator.rb +++ b/lib/rails/generators/couchbase_orm/config/config_generator.rb @@ -1,27 +1,26 @@ -# encoding: utf-8 +# frozen_string_literal: true require 'rails/generators/couchbase_orm_generator' module CouchbaseOrm - module Generators - class ConfigGenerator < Rails::Generators::Base - desc 'Creates a Couchbase configuration file at config/couchbase.yml' - argument :bucket_name, type: :string, optional: true - argument :username, type: :string, optional: true - argument :password, type: :string, optional: true + module Generators + class ConfigGenerator < Rails::Generators::Base + desc 'Creates a Couchbase configuration file at config/couchbase.yml' + argument :bucket_name, type: :string, optional: true + argument :username, type: :string, optional: true + argument :password, type: :string, optional: true - def self.source_root - @_couchbase_source_root ||= File.expand_path('../templates', __FILE__) - end + def self.source_root + @_couchbase_source_root ||= File.expand_path('templates', __dir__) + end - def app_name - Rails::Application.subclasses.first.parent.to_s.underscore - end + def app_name + Rails::Application.subclasses.first.parent.to_s.underscore + end - def create_config_file - template 'couchbase.yml', File.join('config', 'couchbase.yml') - end - - end + def create_config_file + template 'couchbase.yml', File.join('config', 'couchbase.yml') + end end + end end diff --git a/lib/rails/generators/couchbase_orm_generator.rb b/lib/rails/generators/couchbase_orm_generator.rb index 93640c99..a6163ec3 100644 --- a/lib/rails/generators/couchbase_orm_generator.rb +++ b/lib/rails/generators/couchbase_orm_generator.rb @@ -1,4 +1,5 @@ -# encoding: utf-8 +# frozen_string_literal: true + # # Author:: Couchbase # Copyright:: 2012 Couchbase, Inc. @@ -20,23 +21,19 @@ require 'rails/generators/named_base' require 'rails/generators/active_model' -module CouchbaseOrm #:nodoc: - module Generators #:nodoc: - - class Base < ::Rails::Generators::NamedBase #:nodoc: - - def self.source_root - @_couchbase_source_root ||= - File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__) - end - - unless methods.include?(:module_namespacing) - def module_namespacing(&block) - yield if block - end - end +module CouchbaseOrm # :nodoc: + module Generators # :nodoc: + class Base < ::Rails::Generators::NamedBase # :nodoc: + def self.source_root + @_couchbase_source_root ||= + File.expand_path("../#{base_name}/#{generator_name}/templates", __FILE__) + end + unless methods.include?(:module_namespacing) + def module_namespacing(&block) + yield if block end - + end end + end end diff --git a/spec/associations_spec.rb b/spec/associations_spec.rb index ff45ed1d..664cdf32 100644 --- a/spec/associations_spec.rb +++ b/spec/associations_spec.rb @@ -1,212 +1,207 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) - +require File.expand_path('support', __dir__) class Parent < CouchbaseOrm::Base - attribute :name + attribute :name end class RandomOtherType < CouchbaseOrm::Base - attribute :name + attribute :name end class Child < CouchbaseOrm::Base - attribute :name + attribute :name - belongs_to :parent, dependent: :destroy + belongs_to :parent, dependent: :destroy end class Assembly < CouchbaseOrm::Base - attribute :name + attribute :name - has_and_belongs_to_many :parts, autosave: true + has_and_belongs_to_many :parts, autosave: true end class Part < CouchbaseOrm::Base - attribute :name + attribute :name - has_and_belongs_to_many :assemblies, dependent: :destroy, autosave: true + has_and_belongs_to_many :assemblies, dependent: :destroy, autosave: true end - describe CouchbaseOrm::Associations do - describe 'belongs_to' do - it "should work with dependent associations" do - parent = Parent.create!(name: 'joe') - child = Child.create!(name: 'bob', parent_id: parent.id) - - 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(Couchbase::Error::DocumentNotFound) - - expect(Parent.find_by_id(id)).to be(nil) - - 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(Couchbase::Error::DocumentNotFound) - expect { parent.save! }.to raise_error(Couchbase::Error::DocumentNotFound) - end - - it "should cache associations" do - parent = Parent.create!(name: 'joe') - child = Child.create!(name: 'bob', parent_id: parent.id) - - id = child.parent.__id__ - expect(parent.__id__).not_to eq(child.parent.__id__) - expect(parent).to eq(child.parent) - expect(child.parent.__id__).to eq(id) - - child.reload - expect(parent).to eq(child.parent) - expect(child.parent.__id__).not_to eq(id) - - child.destroy - end - - it "should ignore associations when delete is used" do - parent = Parent.create!(name: 'joe') - child = Child.create!(name: 'bob', parent_id: parent.id) - - id = child.id - child.delete - - expect(Child.exists?(id)).to be(false) - expect(Parent.exists?(parent.id)).to be(true) - - id = parent.id - parent.delete - expect(Parent.exists?(id)).to be(false) - end - - it "should raise an error if an invalid type is being assigned" do - begin - parent = RandomOtherType.create!(name: 'joe') - expect { Child.create!(name: 'bob', parent: parent) }.to raise_error(ArgumentError) - ensure - parent.delete - end - end - - describe Parent do - it_behaves_like "ActiveModel" - end - - describe Child do - it_behaves_like "ActiveModel" - end - end - - describe 'has_and_belongs_to_many' do - it "should work with dependent associations" do - assembly = Assembly.create!(name: 'a1') - part = Part.create!(name: 'p1', assemblies: [assembly]) - assembly.reload - - expect(assembly.persisted?).to be(true) - expect(part.persisted?).to be(true) - - part.destroy - expect(part.destroyed?).to be(true) - expect(assembly.destroyed?).to be(true) - end - - it "should cache associations" do - assembly = Assembly.create!(name: 'a1') - part = Part.create!(name: 'p1', assembly_ids: [assembly.id]) - assembly.reload - - id = part.assemblies.first.__id__ - expect(assembly.__id__).not_to eq(part.assemblies.first.__id__) - expect(assembly).to eq(part.assemblies.first) - expect(part.assemblies.first.__id__).to eq(id) - - part.reload - expect(assembly).to eq(part.assemblies.first) - expect(part.assemblies.first.__id__).not_to eq(id) - - part.destroy - end - - it "should ignore associations when delete is used" do - assembly = Assembly.create!(name: 'a1') - part = Part.create!(name: 'p1', assembly_ids: [assembly.id]) - assembly.reload - - id = part.id - part.delete - - expect(Part.exists?(id)).to be(false) - expect(Assembly.exists?(assembly.id)).to be(true) - - id = assembly.id - assembly.delete - expect(Assembly.exists?(id)).to be(false) - end - - it "should raise an error if an invalid type is being assigned" do - begin - assembly = RandomOtherType.create!(name: 'a1') - expect { Part.create!(name: 'p1', assemblies: [assembly]) }.to raise_error(ArgumentError) - ensure - assembly.delete - end - end - - it "should add association with single" do - assembly = Assembly.create!(name: 'a1') - part = Part.create!(name: 'p1', assemblies: [assembly]) - - expect(assembly.reload.parts.map(&:id)).to match_array([part.id]) - end - - it 'should add association with multiple' do - assembly = Assembly.create!(name: 'a1') - part1 = Part.create!(name: 'p1', assemblies: [assembly]) - part2 = Part.create!(name: 'p2', assemblies: [assembly]) - - expect(assembly.reload.parts.map(&:id)).to match_array([part1.id, part2.id]) - end - - it "should remove association with single" do - assembly1 = Assembly.create!(name: 'a1') - assembly2 = Assembly.create!(name: 'a2') - part = Part.create!(name: 'p1', assemblies: [assembly1]) - part.assemblies = [assembly2] - part.save! - - expect(assembly1.reload.parts.map(&:id)).to be_empty - expect(assembly2.reload.parts.map(&:id)).to match_array([part.id]) - end - - it 'should remove association with multiple' do - assembly1 = Assembly.create!(name: 'a1') - assembly2 = Assembly.create!(name: 'a2') - part1 = Part.create!(name: 'p1', assemblies: [assembly1]) - part2 = Part.create!(name: 'p2', assemblies: [assembly2]) - - part1.assemblies = part1.assemblies + [assembly2] - part1.save! - - expect(assembly1.reload.parts.map(&:id)).to match_array([part1.id]) - expect(assembly2.reload.parts.map(&:id)).to match_array([part1.id, part2.id]) - end - - describe Assembly do - it_behaves_like "ActiveModel" - end - - describe Part do - it_behaves_like "ActiveModel" - end + describe 'belongs_to' do + it 'works with dependent associations' do + parent = Parent.create!(name: 'joe') + child = Child.create!(name: 'bob', parent_id: parent.id) + + 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(Couchbase::Error::DocumentNotFound) + + expect(Parent.find_by_id(id)).to be_nil + + 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(Couchbase::Error::DocumentNotFound) + expect { parent.save! }.to raise_error(Couchbase::Error::DocumentNotFound) + end + + it 'caches associations' do + parent = Parent.create!(name: 'joe') + child = Child.create!(name: 'bob', parent_id: parent.id) + + id = child.parent.__id__ + expect(parent.__id__).not_to eq(child.parent.__id__) + expect(parent).to eq(child.parent) + expect(child.parent.__id__).to eq(id) + + child.reload + expect(parent).to eq(child.parent) + expect(child.parent.__id__).not_to eq(id) + + child.destroy + end + + it 'ignores associations when delete is used' do + parent = Parent.create!(name: 'joe') + child = Child.create!(name: 'bob', parent_id: parent.id) + + id = child.id + child.delete + + expect(Child.exists?(id)).to be(false) + expect(Parent.exists?(parent.id)).to be(true) + + id = parent.id + parent.delete + expect(Parent.exists?(id)).to be(false) + end + + it 'raises an error if an invalid type is being assigned' do + parent = RandomOtherType.create!(name: 'joe') + expect { Child.create!(name: 'bob', parent: parent) }.to raise_error(ArgumentError) + ensure + parent.delete + end + + describe Parent do + it_behaves_like 'ActiveModel' + end + + describe Child do + it_behaves_like 'ActiveModel' + end + end + + describe 'has_and_belongs_to_many' do + it 'works with dependent associations' do + assembly = Assembly.create!(name: 'a1') + part = Part.create!(name: 'p1', assemblies: [assembly]) + assembly.reload + + expect(assembly.persisted?).to be(true) + expect(part.persisted?).to be(true) + + part.destroy + expect(part.destroyed?).to be(true) + expect(assembly.destroyed?).to be(true) + end + + it 'caches associations' do + assembly = Assembly.create!(name: 'a1') + part = Part.create!(name: 'p1', assembly_ids: [assembly.id]) + assembly.reload + + id = part.assemblies.first.__id__ + expect(assembly.__id__).not_to eq(part.assemblies.first.__id__) + expect(assembly).to eq(part.assemblies.first) + expect(part.assemblies.first.__id__).to eq(id) + + part.reload + expect(assembly).to eq(part.assemblies.first) + expect(part.assemblies.first.__id__).not_to eq(id) + + part.destroy + end + + it 'ignores associations when delete is used' do + assembly = Assembly.create!(name: 'a1') + part = Part.create!(name: 'p1', assembly_ids: [assembly.id]) + assembly.reload + + id = part.id + part.delete + + expect(Part.exists?(id)).to be(false) + expect(Assembly.exists?(assembly.id)).to be(true) + + id = assembly.id + assembly.delete + expect(Assembly.exists?(id)).to be(false) + end + + it 'raises an error if an invalid type is being assigned' do + assembly = RandomOtherType.create!(name: 'a1') + expect { Part.create!(name: 'p1', assemblies: [assembly]) }.to raise_error(ArgumentError) + ensure + assembly.delete + end + + it 'adds association with single' do + assembly = Assembly.create!(name: 'a1') + part = Part.create!(name: 'p1', assemblies: [assembly]) + + expect(assembly.reload.parts.map(&:id)).to contain_exactly(part.id) + end + + it 'adds association with multiple' do + assembly = Assembly.create!(name: 'a1') + part1 = Part.create!(name: 'p1', assemblies: [assembly]) + part2 = Part.create!(name: 'p2', assemblies: [assembly]) + + expect(assembly.reload.parts.map(&:id)).to contain_exactly(part1.id, part2.id) + end + + it 'removes association with single' do + assembly1 = Assembly.create!(name: 'a1') + assembly2 = Assembly.create!(name: 'a2') + part = Part.create!(name: 'p1', assemblies: [assembly1]) + part.assemblies = [assembly2] + part.save! + + expect(assembly1.reload.parts.map(&:id)).to be_empty + expect(assembly2.reload.parts.map(&:id)).to contain_exactly(part.id) + end + + it 'removes association with multiple' do + assembly1 = Assembly.create!(name: 'a1') + assembly2 = Assembly.create!(name: 'a2') + part1 = Part.create!(name: 'p1', assemblies: [assembly1]) + part2 = Part.create!(name: 'p2', assemblies: [assembly2]) + + part1.assemblies = part1.assemblies + [assembly2] + part1.save! + + expect(assembly1.reload.parts.map(&:id)).to contain_exactly(part1.id) + expect(assembly2.reload.parts.map(&:id)).to contain_exactly(part1.id, part2.id) + end + + describe Assembly do + it_behaves_like 'ActiveModel' + end + + describe Part do + it_behaves_like 'ActiveModel' end + end end diff --git a/spec/attribute_dynamic_spec.rb b/spec/attribute_dynamic_spec.rb index 9fa2a438..e90c6333 100644 --- a/spec/attribute_dynamic_spec.rb +++ b/spec/attribute_dynamic_spec.rb @@ -1,27 +1,28 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) class AttributeDynamicTest < CouchbaseOrm::Base - include CouchbaseOrm::AttributesDynamic + include CouchbaseOrm::AttributesDynamic - attribute :name, :string - attribute :job, :string + attribute :name, :string + attribute :job, :string end describe CouchbaseOrm::AttributesDynamic do - context 'from initialize' do - it 'should accept unknown attribute from initialize' do - dynamic = AttributeDynamicTest.new(name: 'joe', new_attribute: 1) - expect(dynamic.new_attribute).to eq(1) - end - end + context 'from initialize' do + it 'accepts unknown attribute from initialize' do + dynamic = AttributeDynamicTest.new(name: 'joe', new_attribute: 1) + expect(dynamic.new_attribute).to eq(1) + end + end - context 'from Couchbase' do - it 'should accept unknown attribute from Couchbase' do - dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: 2) - expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: 2) - dynamic.destroy - end - end -end \ No newline at end of file + context 'from Couchbase' do + it 'accepts unknown attribute from Couchbase' do + dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: 2) + expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: 2) + dynamic.destroy + end + end +end diff --git a/spec/base_spec.rb b/spec/base_spec.rb index b78038a1..419d3ecf 100644 --- a/spec/base_spec.rb +++ b/spec/base_spec.rb @@ -1,281 +1,271 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) class BaseTest < CouchbaseOrm::Base - attribute :name, :string - attribute :job, :string + attribute :name, :string + attribute :job, :string end class CompareTest < CouchbaseOrm::Base - attribute :age, :integer + attribute :age, :integer end class TimestampTest < CouchbaseOrm::Base - attribute :created_at, :datetime, precision: 6 - attribute :deleted_at, :datetime, precision: 6 + attribute :created_at, :datetime, precision: 6 + attribute :deleted_at, :datetime, precision: 6 end class BaseTestWithIgnoredProperties < CouchbaseOrm::Base - ignored_properties :deprecated_property - attribute :name, :string - attribute :job, :string + ignored_properties :deprecated_property + attribute :name, :string + attribute :job, :string end describe CouchbaseOrm::Base do - it "should be comparable to other objects" do - base = BaseTest.create!(name: 'joe') - base2 = BaseTest.create!(name: 'joe') - base3 = BaseTest.create!(ActiveSupport::HashWithIndifferentAccess.new(name: 'joe')) - - expect(base).to eq(base) - expect(base).to be(base) - expect(base).not_to eq(base2) - - same_base = BaseTest.find(base.id) - expect(base).to eq(same_base) - expect(base).not_to be(same_base) - expect(base2).not_to eq(same_base) - - base.delete - base2.delete - base3.delete + it 'is comparable to other objects' do + base = BaseTest.create!(name: 'joe') + base2 = BaseTest.create!(name: 'joe') + base3 = BaseTest.create!(ActiveSupport::HashWithIndifferentAccess.new(name: 'joe')) + + expect(base).to eq(base) + expect(base).to be(base) + expect(base).not_to eq(base2) + + same_base = BaseTest.find(base.id) + expect(base).to eq(same_base) + expect(base).not_to be(same_base) + expect(base2).not_to eq(same_base) + + base.delete + base2.delete + base3.delete + end + + it 'is inspectable' do + base = BaseTest.create!(name: 'joe') + expect(base.inspect).to eq("#") + end + + it 'loads database responses' do + base = BaseTest.create!(name: 'joe') + resp = BaseTest.bucket.default_collection.get(base.id) + + base_loaded = BaseTest.new(resp, id: base.id) + + expect(base_loaded.id).to eq(base.id) + expect(base_loaded).to eq(base) + expect(base_loaded).not_to be(base) + + base.destroy + end + + it 'does 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(CouchbaseOrm::Error::TypeMismatchError) + + base.destroy + end + + it 'raises ActiveModel::UnknownAttributeError on loading objects with unexpected properties' do + too_much_properties_doc = { + type: BaseTest.design_document, + name: 'Pierre', + job: 'dev', + age: '42' + } + BaseTest.bucket.default_collection.upsert 'doc_1', too_much_properties_doc + + expect { BaseTest.find_by_id('doc_1') }.to raise_error(ActiveModel::UnknownAttributeError) + + BaseTest.bucket.default_collection.remove 'doc_1' + end + + it 'loads objects even if there is a missing property in doc' do + missing_properties_doc = { + type: BaseTest.design_document, + name: 'Pierre' + } + BaseTest.bucket.default_collection.upsert 'doc_1', missing_properties_doc + base = BaseTest.find('doc_1') + + expect(base.name).to eq('Pierre') + expect(base.job).to be_nil + base.destroy + end + + it 'supports serialisation' do + base = BaseTest.create!(name: 'joe') + + base_id = base.id + 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 + end + + it 'supports dirty attributes' do + base = BaseTest.new + expect(base.changes.empty?).to be(true) + expect(base.previous_changes.empty?).to be(true) + + 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) + + # 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(true) + + # 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.persisted? + end + + it 'tries to load a model with nothing but an ID' do + base = BaseTest.create!(name: 'joe') + obj = CouchbaseOrm.try_load(base.id) + expect(obj).to eq(base) + ensure + base.destroy + end + + it 'is able to create model with a custom ID' do + base = BaseTest.create!(id: 'custom_id', name: 'joe') + expect(base.id).to eq('custom_id') + + base = BaseTest.find('custom_id') + expect(base.id).to eq('custom_id') + ensure + base&.destroy + end + + it 'tries to load a model with nothing but single-multiple ID' do + bases = [BaseTest.create!(name: 'joe')] + objs = CouchbaseOrm.try_load(bases.map(&:id)) + expect(objs).to match_array(bases) + ensure + bases.each(&:destroy) + end + + it 'tries to load a model with nothing but multiple ID' do + bases = [BaseTest.create!(name: 'joe'), CompareTest.create!(age: 12)] + objs = CouchbaseOrm.try_load(bases.map(&:id)) + expect(objs).to match_array(bases) + ensure + bases.each(&:destroy) + end + + it 'sets the attribute on creation' do + base = BaseTest.create!(name: 'joe') + expect(base.name).to eq('joe') + ensure + base.destroy + end + + it 'supports getting the attribute by key' do + base = BaseTest.create!(name: 'joe') + expect(base[:name]).to eq('joe') + ensure + base.destroy + end + + it 'cannot change the id of a loaded object' do + base = BaseTest.create!(name: 'joe') + expect(base.id).not_to be_nil + expect{ base.id = 'foo' }.to raise_error(RuntimeError, 'ID cannot be changed') + end + + it 'attributes should be HashWithIndifferentAccess' do + base = BaseTest.create!(name: 'joe') + expect(base.attributes.class).to be(HashWithIndifferentAccess) + end + + if ActiveModel::VERSION::MAJOR >= 6 + it 'has timestamp attributes for create in model' do + expect(TimestampTest.timestamp_attributes_for_create_in_model).to eq(['created_at']) end + end - it "should be inspectable" do - base = BaseTest.create!(name: 'joe') - expect(base.inspect).to eq("#") - end + it 'generates a timestamp on creation' do + base = TimestampTest.create! + expect(base.created_at).to be_a(Time) + end - it "should load database responses" do - base = BaseTest.create!(name: 'joe') - resp = BaseTest.bucket.default_collection.get(base.id) + it 'raises error when get object by nil id with quiet as false' do + expect { BaseTest.find(nil, quiet: false) }.to raise_error(CouchbaseOrm::Error::EmptyNotAllowed) + end - base_loaded = BaseTest.new(resp, id: base.id) + it 'does not raise error when get object by nil id with quiet as true' do + expect { BaseTest.find(nil, quiet: true) }.not_to raise_error + end - expect(base_loaded.id).to eq(base.id) - expect(base_loaded).to eq(base) - expect(base_loaded).not_to be(base) + it 'does not mark object as dirty on get' do + base = BaseTest.create!(name: 'joe') - base.destroy - end + expect(BaseTest.find_by_id(base.id).changes).to be_empty - it "should not load objects if there is a type mismatch" do - base = BaseTest.create!(name: 'joe') + base.destroy + end - expect { CompareTest.find_by_id(base.id) }.to raise_error(CouchbaseOrm::Error::TypeMismatchError) + describe BaseTest do + it_behaves_like 'ActiveModel' + end - base.destroy - end + describe CompareTest do + it_behaves_like 'ActiveModel' + end - it "raises ActiveModel::UnknownAttributeError on loading objects with unexpected properties" do - too_much_properties_doc = { - type: BaseTest.design_document, - name: 'Pierre', - job: 'dev', - age: '42' - } - BaseTest.bucket.default_collection.upsert 'doc_1', too_much_properties_doc - - expect { BaseTest.find_by_id('doc_1') }.to raise_error(ActiveModel::UnknownAttributeError) - - BaseTest.bucket.default_collection.remove 'doc_1' + describe '.ignored_properties' do + it 'returns an array of ignored properties' do + expect(BaseTestWithIgnoredProperties.ignored_properties).to eq(['deprecated_property']) end - it "loads objects even if there is a missing property in doc" do - missing_properties_doc = { - type: BaseTest.design_document, - name: 'Pierre' + context 'given a document with ignored properties' do + let(:doc_id) { 'doc_1' } + let(:document_properties) do + { + 'type' => BaseTestWithIgnoredProperties.design_document, + 'name' => 'Pierre', + 'job' => 'dev', + 'deprecated_property' => 'depracted that could be removed on next save' } - BaseTest.bucket.default_collection.upsert 'doc_1', missing_properties_doc - base = BaseTest.find('doc_1') - - expect(base.name).to eq('Pierre') - expect(base.job).to be_nil - base.destroy - end - - it "should support serialisation" do - base = BaseTest.create!(name: 'joe') - - base_id = base.id - 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 - end - - it "should support dirty attributes" do - begin - base = BaseTest.new - expect(base.changes.empty?).to be(true) - expect(base.previous_changes.empty?).to be(true) - - 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) - - # 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(true) - - # 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.persisted? - end - end - - it "should try to load a model with nothing but an ID" do - begin - base = BaseTest.create!(name: 'joe') - obj = CouchbaseOrm.try_load(base.id) - expect(obj).to eq(base) - ensure - base.destroy - end - end - - it "should be able to create model with a custom ID" do - begin - base = BaseTest.create!(id: 'custom_id', name: 'joe') - expect(base.id).to eq('custom_id') - - base = BaseTest.find('custom_id') - expect(base.id).to eq('custom_id') - ensure - base&.destroy - end - end - - - it "should try to load a model with nothing but single-multiple ID" do - begin - bases = [BaseTest.create!(name: 'joe')] - objs = CouchbaseOrm.try_load(bases.map(&:id)) - expect(objs).to match_array(bases) - ensure - bases.each(&:destroy) - end - end - - it "should try to load a model with nothing but multiple ID" do - begin - bases = [BaseTest.create!(name: 'joe'), CompareTest.create!(age: 12)] - objs = CouchbaseOrm.try_load(bases.map(&:id)) - expect(objs).to match_array(bases) - ensure - bases.each(&:destroy) - 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 - - it "cannot change the id of a loaded object" do - base = BaseTest.create!(name: 'joe') - expect(base.id).to_not be_nil - expect{base.id = "foo"}.to raise_error(RuntimeError, 'ID cannot be changed') - end - - it "attributes should be HashWithIndifferentAccess" do - base = BaseTest.create!(name: 'joe') - expect(base.attributes.class).to be(HashWithIndifferentAccess) - 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 - - it 'should raise error when get object by nil id with quiet as false' do - expect { BaseTest.find(nil, quiet: false) }.to raise_error(CouchbaseOrm::Error::EmptyNotAllowed) - end - - it 'should not raise error when get object by nil id with quiet as true' do - expect { BaseTest.find(nil, quiet: true) }.not_to raise_error - end - - it 'should not mark object as dirty on get' do - base = BaseTest.create!(name: 'joe') - - expect(BaseTest.find_by_id(base.id).changes).to be_empty - - base.destroy - end - - describe BaseTest do - it_behaves_like "ActiveModel" - end - - describe CompareTest do - it_behaves_like "ActiveModel" - end - - describe '.ignored_properties' do - - - it 'returns an array of ignored properties' do - expect(BaseTestWithIgnoredProperties.ignored_properties).to eq(['deprecated_property']) - end - - context 'given a document with ignored properties' do - let(:doc_id) { 'doc_1' } - let(:document_properties) do - { - 'type' => BaseTestWithIgnoredProperties.design_document, - 'name' => 'Pierre', - 'job' => 'dev', - 'deprecated_property' => 'depracted that could be removed on next save' - } - end - let(:loaded_model) { BaseTestWithIgnoredProperties.find(doc_id) } - - before { BaseTestWithIgnoredProperties.bucket.default_collection.upsert doc_id, document_properties } - after { BaseTestWithIgnoredProperties.bucket.default_collection.remove doc_id } - - it 'ignores the ignored properties on load from db (and dont raise)' do - expect(loaded_model.attributes.keys).not_to include('deprecated_property') - expect(loaded_model.name).to eq('Pierre') - expect(BaseTestWithIgnoredProperties.bucket.default_collection.get(doc_id).content).to include(document_properties) - end - - # TODO: deprecated, need to rework - xit 'delete the ignored properties on save' do - base = BaseTestWithIgnoredProperties.find(doc_id) - expect{ loaded_model.save }.to change { BaseTestWithIgnoredProperties.bucket.default_collection.get(doc_id).content.keys.sort }. - from(%w[deprecated_property job name type]). - to(%w[job name type]) - end - end + end + let(:loaded_model) { BaseTestWithIgnoredProperties.find(doc_id) } + + before { BaseTestWithIgnoredProperties.bucket.default_collection.upsert doc_id, document_properties } + after { BaseTestWithIgnoredProperties.bucket.default_collection.remove doc_id } + + it 'ignores the ignored properties on load from db (and dont raise)' do + expect(loaded_model.attributes.keys).not_to include('deprecated_property') + expect(loaded_model.name).to eq('Pierre') + expect(BaseTestWithIgnoredProperties.bucket.default_collection.get(doc_id).content).to include(document_properties) + end + + # TODO: deprecated, need to rework + xit 'delete the ignored properties on save' do + base = BaseTestWithIgnoredProperties.find(doc_id) + expect{ loaded_model.save }.to change { + BaseTestWithIgnoredProperties.bucket.default_collection.get(doc_id).content.keys.sort + } + .from(%w[deprecated_property job name type]) + .to(%w[job name type]) + end end + end end diff --git a/spec/collection_proxy_spec.rb b/spec/collection_proxy_spec.rb index c6030968..40fa401c 100644 --- a/spec/collection_proxy_spec.rb +++ b/spec/collection_proxy_spec.rb @@ -1,29 +1,36 @@ -require File.expand_path("../support", __FILE__) -require File.expand_path("../../lib/couchbase-orm/proxies/collection_proxy", __FILE__) +# frozen_string_literal: true + +require File.expand_path('support', __dir__) +require File.expand_path('../lib/couchbase-orm/proxies/collection_proxy', __dir__) class Proxyfied - def get(key, options = nil) - raise Couchbase::Error::DocumentNotFound - end - def remove(key, options = nil) - raise Couchbase::Error::DocumentNotFound - end + 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 'raises 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 'does not raise an error when get is called with non bang version' do + expect { CouchbaseOrm::CollectionProxy.new(Proxyfied.new).get('key') }.not_to 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 'raises 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 + it 'does not raise an error when remove is called with non bang version' do + expect { CouchbaseOrm::CollectionProxy.new(Proxyfied.new).remove('key') }.not_to raise_error + end end diff --git a/spec/connection_spec.rb b/spec/connection_spec.rb index 1224a4f0..22ec8113 100644 --- a/spec/connection_spec.rb +++ b/spec/connection_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) class ConnectedModel < CouchbaseOrm::Base attribute :name, :string @@ -9,19 +10,19 @@ class ConnectedModel < CouchbaseOrm::Base # disabled by default because a little hacky # and test couchbase ruby client not couchbase orm -return unless ENV["TEST_DOCKER_CONTAINER"] +return unless ENV['TEST_DOCKER_CONTAINER'] describe CouchbaseOrm::Base do - it "should reconnect after a disconnection" do - s = ConnectedModel.create!(name: "foo") - `docker stop #{ENV["TEST_DOCKER_CONTAINER"]}` - sleep 3 - expect {ConnectedModel.find(s.id)}.to raise_error(Couchbase::Error::UnambiguousTimeout) - `docker start #{ENV["TEST_DOCKER_CONTAINER"]}` - sleep 10 - s2 = ConnectedModel.find(s.id) - expect(s2.name).to eq (s.name) - ensure - `docker start #{ENV["TEST_DOCKER_CONTAINER"]}` - end + it 'reconnects after a disconnection' do + s = ConnectedModel.create!(name: 'foo') + `docker stop #{ENV['TEST_DOCKER_CONTAINER']}` + sleep 3 + expect { ConnectedModel.find(s.id) }.to raise_error(Couchbase::Error::UnambiguousTimeout) + `docker start #{ENV['TEST_DOCKER_CONTAINER']}` + sleep 10 + s2 = ConnectedModel.find(s.id) + expect(s2.name).to eq s.name + ensure + `docker start #{ENV['TEST_DOCKER_CONTAINER']}` + end end diff --git a/spec/enum_spec.rb b/spec/enum_spec.rb index 76036183..05dec065 100644 --- a/spec/enum_spec.rb +++ b/spec/enum_spec.rb @@ -1,34 +1,34 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) class EnumTest < CouchbaseOrm::Base - enum rating: [:awesome, :good, :okay, :bad], default: :okay - enum color: [:red, :green, :blue] + 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 'creates 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 'sets the attribute' do + base = EnumTest.create!(rating: :good, color: :red) + expect(base.rating).not_to be_nil + expect(base.color).not_to 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 'converts 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 + it 'uses 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 68dbc417..787da923 100644 --- a/spec/has_many_spec.rb +++ b/spec/has_many_spec.rb @@ -1,129 +1,129 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) -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) +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) - @rating_test_class.ensure_design_document! - @object_test_class.ensure_design_document! - @object_rating_test_class.ensure_design_document! + @rating_test_class.ensure_design_document! + @object_test_class.ensure_design_document! + @object_rating_test_class.ensure_design_document! - @rating_test_class.delete_all - @object_test_class.delete_all - @object_rating_test_class.delete_all - end + @rating_test_class.delete_all + @object_test_class.delete_all + @object_rating_test_class.delete_all + end - after :each do - @rating_test_class.delete_all - @object_test_class.delete_all - @object_rating_test_class.delete_all - end + after do + @rating_test_class.delete_all + @object_test_class.delete_all + @object_rating_test_class.delete_all + end - it "should return matching results" do - first = @object_test_class.create! name: :bob - second = @object_test_class.create! name: :jane + it 'returns matching results' do + first = @object_test_class.create! name: :bob + second = @object_test_class.create! name: :jane - rate = @rating_test_class.create! rating: :awesome, "object_#{@context}_test": first - @rating_test_class.create! rating: :bad, "object_#{@context}_test": second - @rating_test_class.create! rating: :good, "object_#{@context}_test": first + rate = @rating_test_class.create! rating: :awesome, "object_#{@context}_test": first + @rating_test_class.create! rating: :bad, "object_#{@context}_test": second + @rating_test_class.create! rating: :good, "object_#{@context}_test": first - expect(rate.try("object_#{@context}_test_id")).to eq(first.id) - expect(@rating_test_class.respond_to?(:"find_by_object_#{@context}_test_id")).to be(true) - expect(first.respond_to?(:"rating_#{@context}_tests")).to be(true) + expect(rate.try("object_#{@context}_test_id")).to eq(first.id) + expect(@rating_test_class.respond_to?(:"find_by_object_#{@context}_test_id")).to be(true) + expect(first.respond_to?(:"rating_#{@context}_tests")).to be(true) - docs = first.try(:"rating_#{@context}_tests").collect(&:rating) + docs = first.try(:"rating_#{@context}_tests").collect(&:rating) - expect(docs).to match_array([1, 2]) + expect(docs).to contain_exactly(1, 2) - first.destroy - expect { @rating_test_class.find rate.id }.to raise_error(Couchbase::Error::DocumentNotFound) - expect(@rating_test_class.send(:"#{@context}_all").count).to be(1) - end + first.destroy + expect { @rating_test_class.find rate.id }.to raise_error(Couchbase::Error::DocumentNotFound) + expect(@rating_test_class.send(:"#{@context}_all").count).to be(1) + end - it "should work through a join model" do - first = @object_test_class.create! name: :bob - second = @object_test_class.create! name: :jane + it 'works through a join model' do + first = @object_test_class.create! name: :bob + 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 + 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 - 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 + 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 - expect(ort.try(:"rating_#{@context}_test_id".to_sym)).to eq(rate1.id) - expect(rate1.respond_to?(:"object_#{@context}_tests")).to be(true) - docs = rate1.try(:"object_#{@context}_tests").collect(&:name) + expect(ort.try(:"rating_#{@context}_test_id".to_sym)).to eq(rate1.id) + expect(rate1.respond_to?(:"object_#{@context}_tests")).to be(true) + docs = rate1.try(:"object_#{@context}_tests").collect(&:name) - expect(docs).to match_array(['bob', 'jane']) - end + expect(docs).to contain_exactly('bob', 'jane') + end - it "should work with new objects not yet saved" do - existing_object = @object_test_class.create! name: :bob - expect(existing_object.send(:"rating_#{@context}_tests")).to be_empty + it 'works with new objects not yet saved' do + existing_object = @object_test_class.create! name: :bob + expect(existing_object.send(:"rating_#{@context}_tests")).to be_empty - @rating_test_class.create! rating: :good, "object_#{@context}_test": existing_object - - new_object = @object_test_class.new name: :jane - expect(new_object.send(:"rating_#{@context}_tests")).to be_empty - end + @rating_test_class.create! rating: :good, "object_#{@context}_test": existing_object + new_object = @object_test_class.new name: :jane + expect(new_object.send(:"rating_#{@context}_tests")).to be_empty + end end describe CouchbaseOrm::HasMany do - context 'with view' do - class ObjectRatingViewTest < CouchbaseOrm::Base - join :object_view_test, :rating_view_test - view :view_all - end - - class RatingViewTest < CouchbaseOrm::Base - enum rating: [:awesome, :good, :okay, :bad], default: :okay - belongs_to :object_view_test + context 'with view' do + class ObjectRatingViewTest < CouchbaseOrm::Base + join :object_view_test, :rating_view_test + view :view_all + end - has_many :object_view_tests, through: :object_rating_view_test - view :view_all - end + class RatingViewTest < CouchbaseOrm::Base + enum rating: [:awesome, :good, :okay, :bad], default: :okay + belongs_to :object_view_test - class ObjectViewTest < CouchbaseOrm::Base - attribute :name, type: String - has_many :rating_view_tests, dependent: :destroy + has_many :object_view_tests, through: :object_rating_view_test + view :view_all + end - view :view_all - end + class ObjectViewTest < CouchbaseOrm::Base + attribute :name, type: String + has_many :rating_view_tests, dependent: :destroy - include_examples("has_many example", context: :view) + view :view_all end - context 'with n1ql' do - class ObjectRatingN1qlTest < CouchbaseOrm::Base - join :object_n1ql_test, :rating_n1ql_test + include_examples('has_many example', context: :view) + end + + context 'with n1ql' do + class ObjectRatingN1qlTest < CouchbaseOrm::Base + join :object_n1ql_test, :rating_n1ql_test - n1ql :n1ql_all - end + n1ql :n1ql_all + end - class RatingN1qlTest < CouchbaseOrm::Base - enum rating: [:awesome, :good, :okay, :bad], default: :okay - belongs_to :object_n1ql_test + class RatingN1qlTest < CouchbaseOrm::Base + enum rating: [:awesome, :good, :okay, :bad], default: :okay + belongs_to :object_n1ql_test - has_many :object_n1ql_tests, through: :object_rating_n1ql_test, type: :n1ql - - n1ql :n1ql_all - end + has_many :object_n1ql_tests, through: :object_rating_n1ql_test, type: :n1ql - class ObjectN1qlTest < CouchbaseOrm::Base - attribute :name, type: String + n1ql :n1ql_all + end - has_many :rating_n1ql_tests, dependent: :destroy, type: :n1ql + class ObjectN1qlTest < CouchbaseOrm::Base + attribute :name, type: String - n1ql :n1ql_all - end + has_many :rating_n1ql_tests, dependent: :destroy, type: :n1ql - include_examples("has_many example", context: :n1ql) + n1ql :n1ql_all end + + include_examples('has_many example', context: :n1ql) + end end diff --git a/spec/id_generator_spec.rb b/spec/id_generator_spec.rb index f1594d6e..dbb15b41 100644 --- a/spec/id_generator_spec.rb +++ b/spec/id_generator_spec.rb @@ -1,48 +1,47 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true require 'couchbase-orm' -require 'thread' - class IdTestModel < CouchbaseOrm::Base; end describe CouchbaseOrm::IdGenerator do - it "should not generate ID clashes" do - model = IdTestModel.new - - ids1 = [] - thread1 = Thread.new do - (1..10000).each { - ids1 << CouchbaseOrm::IdGenerator.next(model) - } - end - - ids2 = [] - thread2 = Thread.new do - (1..10000).each { - ids2 << CouchbaseOrm::IdGenerator.next(model) - } - end - - ids3 = [] - thread3 = Thread.new do - (1..10000).each { - ids3 << CouchbaseOrm::IdGenerator.next(model) - } - end - - ids4 = [] - thread4 = Thread.new do - (1..10000).each { - ids4 << CouchbaseOrm::IdGenerator.next(model) - } - end - - thread1.join - thread2.join - thread3.join - thread4.join - - results = [ids1, ids2, ids3, ids4].flatten - expect(results.uniq).to eq(results) + it 'does not generate ID clashes' do + model = IdTestModel.new + + ids1 = [] + thread1 = Thread.new do + 10000.times { + ids1 << CouchbaseOrm::IdGenerator.next(model) + } + end + + ids2 = [] + thread2 = Thread.new do + 10000.times { + ids2 << CouchbaseOrm::IdGenerator.next(model) + } + end + + ids3 = [] + thread3 = Thread.new do + 10000.times { + ids3 << CouchbaseOrm::IdGenerator.next(model) + } end + + ids4 = [] + thread4 = Thread.new do + 10000.times { + ids4 << CouchbaseOrm::IdGenerator.next(model) + } + end + + thread1.join + thread2.join + thread3.join + thread4.join + + results = [ids1, ids2, ids3, ids4].flatten + expect(results.uniq).to eq(results) + end end diff --git a/spec/index_spec.rb b/spec/index_spec.rb index c9d0a26a..06962d78 100644 --- a/spec/index_spec.rb +++ b/spec/index_spec.rb @@ -1,142 +1,141 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) - +require File.expand_path('support', __dir__) class IndexTest < CouchbaseOrm::Base - attribute :email, type: String - attribute :name, type: String, default: :joe - ensure_unique :email, presence: false + attribute :email, type: String + attribute :name, type: String, default: :joe + ensure_unique :email, presence: false end class NoUniqueIndexTest < CouchbaseOrm::Base - attribute :email, type: String - attribute :name, type: String, default: :joe + attribute :email, type: String + attribute :name, type: String, default: :joe - index :email, presence: false + index :email, presence: false end class IndexEnumTest < CouchbaseOrm::Base - enum visibility: [:group, :authority, :public], default: :authority - enum color: [:red, :green, :blue] + enum visibility: [:group, :authority, :public], default: :authority + enum color: [:red, :green, :blue] end - describe CouchbaseOrm::Index do - after :each do - IndexTest.all.map(&:destroy) - end + after do + IndexTest.all.map(&:destroy) + end - it "should prevent models being created if they should have unique keys" do - joe = IndexTest.create!(email: 'joe@aca.com') - expect { IndexTest.create!(email: 'joe@aca.com') }.to raise_error(CouchbaseOrm::Error::RecordInvalid) + it 'prevents models being created if they should have unique keys' do + joe = IndexTest.create!(email: 'joe@aca.com') + expect { IndexTest.create!(email: 'joe@aca.com') }.to raise_error(CouchbaseOrm::Error::RecordInvalid) - joe.email = 'other@aca.com' - joe.save - other = IndexTest.new(email: 'joe@aca.com') - expect(other.save).to be(true) + joe.email = 'other@aca.com' + joe.save + other = IndexTest.new(email: 'joe@aca.com') + expect(other.save).to be(true) - expect { IndexTest.create!(email: 'joe@aca.com') }.to raise_error(CouchbaseOrm::Error::RecordInvalid) - expect { IndexTest.create!(email: 'other@aca.com') }.to raise_error(CouchbaseOrm::Error::RecordInvalid) + expect { IndexTest.create!(email: 'joe@aca.com') }.to raise_error(CouchbaseOrm::Error::RecordInvalid) + expect { IndexTest.create!(email: 'other@aca.com') }.to raise_error(CouchbaseOrm::Error::RecordInvalid) - joe.destroy - other.destroy + joe.destroy + other.destroy - again = IndexTest.new(email: 'joe@aca.com') - expect(again.save).to be(true) + again = IndexTest.new(email: 'joe@aca.com') + expect(again.save).to be(true) - again.destroy - end + again.destroy + end - it "should provide helper methods for looking up the model" do - joe = IndexTest.create!(email: 'joe@aca.com') + it 'provides helper methods for looking up the model' do + joe = IndexTest.create!(email: 'joe@aca.com') - joe_again = IndexTest.find_by_email('joe@aca.com') - expect(joe).to eq(joe_again) + joe_again = IndexTest.find_by_email('joe@aca.com') + expect(joe).to eq(joe_again) - joe.destroy - end + joe.destroy + end - it "should clean up itself if dangling keys are left" do - joe = IndexTest.create!(email: 'joe@aca.com') - joe.delete # no callbacks are executed + it 'cleans up itself if dangling keys are left' do + joe = IndexTest.create!(email: 'joe@aca.com') + joe.delete # no callbacks are executed - again = IndexTest.new(email: 'joe@aca.com') - expect(again.save).to be(true) + again = IndexTest.new(email: 'joe@aca.com') + expect(again.save).to be(true) - again.destroy - end + again.destroy + end - it "should work with nil values" do - joe = IndexTest.create! - expect(IndexTest.find_by_email(nil)).to eq(joe) + it 'works with nil values' do + joe = IndexTest.create! + expect(IndexTest.find_by_email(nil)).to eq(joe) - joe.email = 'joe@aca.com' - joe.save! - expect(IndexTest.find_by_email('joe@aca.com')).to eq(joe) + joe.email = 'joe@aca.com' + joe.save! + expect(IndexTest.find_by_email('joe@aca.com')).to eq(joe) - joe.email = nil - joe.save! - expect(IndexTest.find_by_email('joe@aca.com')).to eq(nil) - expect(IndexTest.find_by_email(nil)).to eq(joe) + joe.email = nil + joe.save! + expect(IndexTest.find_by_email('joe@aca.com')).to eq(nil) + expect(IndexTest.find_by_email(nil)).to eq(joe) - joe.destroy - end + joe.destroy + end - it "should work with enumerators" do - # Test symbol - enum = IndexEnumTest.create!(visibility: :public) - expect(enum.visibility).to eq(3) - enum.destroy + it 'works with enumerators' do + # Test symbol + enum = IndexEnumTest.create!(visibility: :public) + expect(enum.visibility).to eq(3) + enum.destroy - # Test number - enum = IndexEnumTest.create!(visibility: 2) - expect(enum.visibility).to eq(2) - enum.destroy + # Test number + enum = IndexEnumTest.create!(visibility: 2) + expect(enum.visibility).to eq(2) + enum.destroy - # Test default - enum = IndexEnumTest.create! - expect(enum.visibility).to eq(2) - enum.destroy + # Test default + enum = IndexEnumTest.create! + expect(enum.visibility).to eq(2) + enum.destroy - # Test default default - enum = IndexEnumTest.create! - expect(enum.color).to eq(1) - end + # Test default default + enum = IndexEnumTest.create! + expect(enum.color).to eq(1) + end - it "should not overwrite index's that do not belong to the current model" do - joe = NoUniqueIndexTest.create! - expect(NoUniqueIndexTest.find_by_email(nil)).to eq(joe) + it "does not overwrite index's that do not belong to the current model" do + joe = NoUniqueIndexTest.create! + expect(NoUniqueIndexTest.find_by_email(nil)).to eq(joe) - joe.email = 'joe@aca.com' - joe.save! - expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe) + joe.email = 'joe@aca.com' + joe.save! + expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe) - joe2 = NoUniqueIndexTest.create! - joe2.email = 'joe@aca.com' # joe here is deliberate - joe2.save! + joe2 = NoUniqueIndexTest.create! + joe2.email = 'joe@aca.com' # joe here is deliberate + joe2.save! - expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe2) + expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe2) - # Joe's indexing should not remove joe2 index - joe.email = nil - joe.save! - expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe2) + # Joe's indexing should not remove joe2 index + joe.email = nil + joe.save! + expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe2) - # Test destroy - joe.email = 'joe@aca.com' - joe.save! - expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe) + # Test destroy + joe.email = 'joe@aca.com' + joe.save! + expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe) - # Index should not be updated - joe2.destroy - expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe) + # Index should not be updated + joe2.destroy + expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(joe) - # index should be updated - joe.email = nil - joe.save! - expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(nil) + # index should be updated + joe.email = nil + joe.save! + expect(NoUniqueIndexTest.find_by_email('joe@aca.com')).to eq(nil) - joe.destroy - end + joe.destroy + end end diff --git a/spec/n1ql_spec.rb b/spec/n1ql_spec.rb index 1418be31..7b68b7fe 100644 --- a/spec/n1ql_spec.rb +++ b/spec/n1ql_spec.rb @@ -1,176 +1,157 @@ # frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) class N1QLTest < CouchbaseOrm::Base - attribute :name, type: String - attribute :lastname, type: String - enum rating: [:awesome, :good, :okay, :bad], default: :okay - - n1ql :by_custom_rating, 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 [1, 2] ORDER BY name ASC", options) - } - n1ql :by_name, emit_key: [:name] - n1ql :by_lastname, emit_key: [:lastname] - n1ql :by_rating_emit, emit_key: :rating - - 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 #{quote(values[0])} ORDER BY name ASC", options) - } - n1ql :by_rating_reverse, emit_key: :rating, custom_order: "name DESC" - n1ql :by_rating_without_docs, emit_key: :rating, include_docs: false + attribute :name, type: String + attribute :lastname, type: String + enum rating: [:awesome, :good, :okay, :bad], default: :okay + + n1ql :by_custom_rating, 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 [1, 2] ORDER BY name ASC", options) + } + n1ql :by_name, emit_key: [:name] + n1ql :by_lastname, emit_key: [:lastname] + n1ql :by_rating_emit, emit_key: :rating + + 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 #{quote(values[0])} ORDER BY name ASC", options) + } + n1ql :by_rating_reverse, emit_key: :rating, custom_order: 'name DESC' + n1ql :by_rating_without_docs, emit_key: :rating, include_docs: false # This generates both: # view :by_rating, emit_key: :rating # def self.find_by_rating(rating); end # also provide this helper function - index_n1ql :rating + index_n1ql :rating end describe CouchbaseOrm::N1ql do - before(:each) do - N1QLTest.delete_all - end - - it "should not allow n1ql to override existing methods" do - expect { N1QLTest.n1ql :all }.to raise_error(ArgumentError) - end - - it "should perform a query and return the n1ql" do - N1QLTest.create! name: :bob - docs = N1QLTest.all.collect { |ob| - ob.name - } - expect(docs).to eq(%w[bob]) - end - - it "should query by non-nil value" do - _anonymous = N1QLTest.create! - bob = N1QLTest.create! name: :bob - - expect(N1QLTest.by_name(key: 'bob').to_a).to eq [bob] - end - - it "should query by nil value" do - anonymous = N1QLTest.create! lastname: "Anonymous" - anonymous_no_property = N1QLTest.create! lastname: "Anonymous without name property" - - CouchbaseOrm::Connection.bucket.default_collection.mutate_in(anonymous_no_property.id, [ - Couchbase::MutateInSpec.remove("name"), - ]) - - anonymous_no_property.reload - - _bob = N1QLTest.create! name: :bob - - expect(N1QLTest.by_name(key: nil).to_a).to match_array [anonymous, anonymous_no_property] - end - - it "should query all when key is not set" do - anonymous = N1QLTest.create! - bob = N1QLTest.create! name: :bob - - expect(N1QLTest.by_name.to_a).to eq [anonymous, bob] - end - - - it "should work with other keys" do - N1QLTest.create! name: :bob, rating: :good - N1QLTest.create! name: :jane, rating: :awesome - N1QLTest.create! name: :greg, rating: :bad - - docs = N1QLTest.by_name(descending: true).collect { |ob| - ob.name - } - expect(docs).to eq(%w[jane greg bob]) - - docs = N1QLTest.by_rating(descending: true).collect { |ob| - ob.rating - } - expect(docs).to eq([4, 2, 1]) - end - - it "should return matching results" 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.find_by_rating(1).collect { |ob| - ob.name - } - - expect(Set.new(docs)).to eq(Set.new(%w[bob jane])) - - docs = N1QLTest.by_custom_rating().collect { |ob| - ob.name - } - - expect(Set.new(docs)).to eq(Set.new(%w[bob jane mel])) - end - - it "should return matching results with reverse order" 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_rating_reverse(key: 1).collect { |ob| - ob.name - } - - expect(docs).to eq(%w[jane bob]) - end - - it "should return matching results without full documents" do - inst_bob = N1QLTest.create! name: :bob, rating: :awesome - inst_jane = N1QLTest.create! name: :jane, rating: :awesome - N1QLTest.create! name: :greg, rating: :bad - N1QLTest.create! name: :mel, rating: :good - - docs = N1QLTest.by_rating_without_docs(key: 1) - - expect(Set.new(docs)).to eq(Set.new([inst_bob.id, inst_jane.id])) - end - - it "should return matching results with nil usage" do - N1QLTest.create! name: :bob, lastname: nil - N1QLTest.create! name: :jane, lastname: "dupond" - - docs = N1QLTest.by_lastname(key: [nil]).collect { |ob| - ob.name - } - expect(docs).to eq(%w[bob]) - end + before do + N1QLTest.delete_all + end - it "should allow storing quoting chars" do - special_name = "O'Leary & Sons \"The best\" \\ between backslash \\" - t = N1QLTest.create! name: special_name, rating: :awesome - expect(N1QLTest.find(t.id).name).to eq(special_name) - expect(N1QLTest.by_name(key: special_name).to_a.first).to eq(t) - expect(N1QLTest.where(name: special_name).to_a.first).to eq(t) - puts N1QLTest.where(name: special_name).to_n1ql - 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 + after(:all) do + N1QLTest.delete_all + end + it 'does not allow n1ql to override existing methods' do + expect { N1QLTest.n1ql :all }.to raise_error(ArgumentError) + end - 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.delete_all - end + it 'performs a query and return the n1ql' do + N1QLTest.create! name: :bob + docs = N1QLTest.all.collect(&:name) + expect(docs).to eq(%w[bob]) + end + + it 'queries by non-nil value' do + _anonymous = N1QLTest.create! + bob = N1QLTest.create! name: :bob + + expect(N1QLTest.by_name(key: 'bob').to_a).to eq [bob] + end + + it 'queries by nil value' do + anonymous = N1QLTest.create! lastname: 'Anonymous' + anonymous_no_property = N1QLTest.create! lastname: 'Anonymous without name property' + + CouchbaseOrm::Connection.bucket.default_collection.mutate_in(anonymous_no_property.id, [ + Couchbase::MutateInSpec.remove('name'), + ]) + + anonymous_no_property.reload + + _bob = N1QLTest.create! name: :bob + + expect(N1QLTest.by_name(key: nil).to_a).to contain_exactly(anonymous, anonymous_no_property) + end + + it 'queries all when key is not set' do + anonymous = N1QLTest.create! + bob = N1QLTest.create! name: :bob + + expect(N1QLTest.by_name.to_a).to eq [anonymous, bob] + end + + it 'works with other keys' do + N1QLTest.create! name: :bob, rating: :good + N1QLTest.create! name: :jane, rating: :awesome + N1QLTest.create! name: :greg, rating: :bad + + docs = N1QLTest.by_name(descending: true).collect(&:name) + expect(docs).to eq(%w[jane greg bob]) + + docs = N1QLTest.by_rating(descending: true).collect(&:rating) + expect(docs).to eq([4, 2, 1]) + end + + it 'returns matching results' 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.find_by_rating(1).collect(&:name) + + expect(Set.new(docs)).to eq(Set.new(%w[bob jane])) + + docs = N1QLTest.by_custom_rating.collect(&:name) + + expect(Set.new(docs)).to eq(Set.new(%w[bob jane mel])) + end + + it 'returns matching results with reverse order' 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_rating_reverse(key: 1).collect(&:name) + + expect(docs).to eq(%w[jane bob]) + end + + it 'returns matching results without full documents' do + inst_bob = N1QLTest.create! name: :bob, rating: :awesome + inst_jane = N1QLTest.create! name: :jane, rating: :awesome + N1QLTest.create! name: :greg, rating: :bad + N1QLTest.create! name: :mel, rating: :good + + docs = N1QLTest.by_rating_without_docs(key: 1) + + expect(Set.new(docs)).to eq(Set.new([inst_bob.id, inst_jane.id])) + end + + it 'returns matching results with nil usage' do + N1QLTest.create! name: :bob, lastname: nil + N1QLTest.create! name: :jane, lastname: 'dupond' + + docs = N1QLTest.by_lastname(key: [nil]).collect(&:name) + expect(docs).to eq(%w[bob]) + end + + it 'allows storing quoting chars' do + special_name = "O'Leary & Sons \"The best\" \\ between backslash \\" + t = N1QLTest.create! name: special_name, rating: :awesome + expect(N1QLTest.find(t.id).name).to eq(special_name) + expect(N1QLTest.by_name(key: special_name).to_a.first).to eq(t) + expect(N1QLTest.where(name: special_name).to_a.first).to eq(t) + puts N1QLTest.where(name: special_name).to_n1ql + end + + it 'returns 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(&:name) + + expect(Set.new(docs)).to eq(Set.new(%w[bob jane mel])) + + docs = N1QLTest.by_custom_rating_values(key: [[1, 2]]).collect(&:name) + + expect(Set.new(docs)).to eq(Set.new(%w[bob jane mel])) + end end diff --git a/spec/persistence_spec.rb b/spec/persistence_spec.rb index 13537f5c..8ec1a6ab 100644 --- a/spec/persistence_spec.rb +++ b/spec/persistence_spec.rb @@ -1,322 +1,336 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) -require "action_controller" +require 'action_controller' class BasicModel < CouchbaseOrm::Base - attribute :name - attribute :address - attribute :age + attribute :name + attribute :address + attribute :age end class ModelWithDefaults < CouchbaseOrm::Base - attribute :name, default: proc { 'bob' } - attribute :address - attribute :age, default: 23 + attribute :name, default: proc { 'bob' } + attribute :address + attribute :age, default: 23 end class ModelWithCallbacks < CouchbaseOrm::Base - attribute :name - attribute :address - attribute :age - - before_create :update_name - before_save :set_address - before_update :set_age - after_initialize do - self.age = 10 - end - before_destroy do - self.name = 'joe' - end - - def update_name; self.name = 'bob'; end - def set_address; self.address = '23'; end - def set_age; self.age = 30; end + attribute :name + attribute :address + attribute :age + + before_create :update_name + before_save :set_address + before_update :set_age + after_initialize do + self.age = 10 + end + before_destroy do + self.name = 'joe' + end + + def update_name + self.name = 'bob' + end + + def set_address + self.address = '23' + end + + def set_age + self.age = 30 + end end class ModelWithValidations < CouchbaseOrm::Base - attribute :name, type: String - attribute :address, type: String - attribute :age, type: :Integer + attribute :name, type: String + attribute :address, type: String + attribute :age, type: :Integer - validates :name, presence: true - validates :age, numericality: { only_integer: true } + validates :name, presence: true + validates :age, numericality: { only_integer: true } end - describe CouchbaseOrm::Persistence do - it "should save a model" do - model = BasicModel.new - - expect(model.new_record?).to be(true) - expect(model.destroyed?).to be(false) - expect(model.persisted?).to be(false) - - model.name = 'bob' - expect(model.name).to eq('bob') - - model.address = 'somewhere' - model.age = 34 - - expect(model.new_record?).to be(true) - expect(model.destroyed?).to be(false) - expect(model.persisted?).to be(false) - - result = model.save - expect(result).to be(true) - - expect(model.new_record?).to be(false) - expect(model.destroyed?).to be(false) - expect(model.persisted?).to be(true) - - model.destroy - expect(model.new_record?).to be(false) - expect(model.destroyed?).to be(true) - expect(model.persisted?).to be(false) - end - - it "should save a model with defaults" do - model = ModelWithDefaults.new - - expect(model.name).to eq('bob') - expect(model.age).to be(23) - expect(model.address).to be(nil) - - expect(model.new_record?).to be(true) - expect(model.destroyed?).to be(false) - expect(model.persisted?).to be(false) - - result = model.save - expect(result).to be(true) - - expect(model.new_record?).to be(false) - expect(model.destroyed?).to be(false) - expect(model.persisted?).to be(true) - - model.destroy - expect(model.new_record?).to be(false) - expect(model.destroyed?).to be(true) - expect(model.persisted?).to be(false) - end - - it "should execute callbacks" do - model = ModelWithCallbacks.new - - # Test initialize - expect(model.name).to be(nil) - expect(model.age).to be(10) - expect(model.address).to be(nil) + it 'saves a model' do + model = BasicModel.new - expect(model.new_record?).to be(true) - expect(model.destroyed?).to be(false) - expect(model.persisted?).to be(false) + expect(model.new_record?).to be(true) + expect(model.destroyed?).to be(false) + expect(model.persisted?).to be(false) - # Test create - result = model.save - expect(result).to be(true) + model.name = 'bob' + expect(model.name).to eq('bob') - expect(model.name).to eq('bob') - expect(model.age).to be(10) - expect(model.address).to eq('23') + model.address = 'somewhere' + model.age = 34 - # Test Update - model.address = 'other' - expect(model.address).to eq('other') - model.save + expect(model.new_record?).to be(true) + expect(model.destroyed?).to be(false) + expect(model.persisted?).to be(false) + + result = model.save + expect(result).to be(true) + + expect(model.new_record?).to be(false) + expect(model.destroyed?).to be(false) + expect(model.persisted?).to be(true) + + model.destroy + expect(model.new_record?).to be(false) + expect(model.destroyed?).to be(true) + expect(model.persisted?).to be(false) + end - expect(model.name).to eq('bob') - expect(model.age).to be(30) - expect(model.address).to eq('23') + it 'saves a model with defaults' do + model = ModelWithDefaults.new - # Test destroy - model.destroy - expect(model.new_record?).to be(false) - expect(model.destroyed?).to be(true) - expect(model.persisted?).to be(false) + expect(model.name).to eq('bob') + expect(model.age).to be(23) + expect(model.address).to be_nil - expect(model.name).to eq('joe') + expect(model.new_record?).to be(true) + expect(model.destroyed?).to be(false) + expect(model.persisted?).to be(false) + + result = model.save + expect(result).to be(true) + + expect(model.new_record?).to be(false) + expect(model.destroyed?).to be(false) + expect(model.persisted?).to be(true) + + model.destroy + expect(model.new_record?).to be(false) + expect(model.destroyed?).to be(true) + expect(model.persisted?).to be(false) + end + + it 'executes callbacks' do + model = ModelWithCallbacks.new + + # Test initialize + expect(model.name).to be_nil + expect(model.age).to be(10) + expect(model.address).to be_nil + + expect(model.new_record?).to be(true) + expect(model.destroyed?).to be(false) + expect(model.persisted?).to be(false) + + # Test create + result = model.save + expect(result).to be(true) + + expect(model.name).to eq('bob') + expect(model.age).to be(10) + expect(model.address).to eq('23') + + # Test Update + model.address = 'other' + expect(model.address).to eq('other') + model.save + + expect(model.name).to eq('bob') + expect(model.age).to be(30) + expect(model.address).to eq('23') + + # Test destroy + model.destroy + expect(model.new_record?).to be(false) + expect(model.destroyed?).to be(true) + expect(model.persisted?).to be(false) + + expect(model.name).to eq('joe') + end + + it 'skips callbacks when updating columns' do + model = ModelWithCallbacks.new + + # Test initialize + expect(model.name).to be_nil + expect(model.age).to be(10) + expect(model.address).to be_nil + + expect(model.new_record?).to be(true) + expect(model.destroyed?).to be(false) + expect(model.persisted?).to be(false) + + # Test create + result = model.save + expect(result).to be(true) + + expect(model.name).to eq('bob') + expect(model.age).to be(10) + expect(model.address).to eq('23') + + # Test Update + model.update_columns(address: 'other') + expect(model.address).to eq('other') + loaded = ModelWithCallbacks.find model.id + expect(loaded.address).to eq('other') + + # Test delete skipping callbacks + model.delete + expect(model.new_record?).to be(false) + expect(model.destroyed?).to be(true) + expect(model.persisted?).to be(false) + + expect(model.name).to eq('bob') + end + + it 'performs validations' do + model = ModelWithValidations.new + + expect(model.valid?).to be(false) + + # Test create + result = model.save + expect(result).to be(false) + expect(model.errors.count).to be(2) + + begin + model.save! + rescue ::CouchbaseOrm::Error::RecordInvalid => e + expect(e.record).to be(model) end - it "should skip callbacks when updating columns" do - model = ModelWithCallbacks.new - - # Test initialize - expect(model.name).to be(nil) - expect(model.age).to be(10) - expect(model.address).to be(nil) - - expect(model.new_record?).to be(true) - expect(model.destroyed?).to be(false) - expect(model.persisted?).to be(false) - - # Test create - result = model.save - expect(result).to be(true) - - expect(model.name).to eq('bob') - expect(model.age).to be(10) - expect(model.address).to eq('23') - - # Test Update - model.update_columns(address: 'other') - expect(model.address).to eq('other') - loaded = ModelWithCallbacks.find model.id - expect(loaded.address).to eq('other') - - # Test delete skipping callbacks - model.delete - expect(model.new_record?).to be(false) - expect(model.destroyed?).to be(true) - expect(model.persisted?).to be(false) - - expect(model.name).to eq('bob') - end - - it "should perform validations" do - model = ModelWithValidations.new - - expect(model.valid?).to be(false) - - # Test create - result = model.save - expect(result).to be(false) - expect(model.errors.count).to be(2) - - begin - model.save! - rescue ::CouchbaseOrm::Error::RecordInvalid => e - expect(e.record).to be(model) - end - - model.name = 'bob' - model.age = 23 - expect(model.valid?).to be(true) - expect(model.save).to be(true) - - # Test update - model.name = nil - expect(model.valid?).to be(false) - expect(model.save).to be(false) - begin - model.save! - rescue ::CouchbaseOrm::Error::RecordInvalid => e - expect(e.record).to be(model) - end - - model.age = '23' # This value will be coerced - model.name = 'joe' - expect(model.valid?).to be(true) - expect(model.save!).to be(model) - - # coercion will fail here - model.age = "a23" - expect{ model.save! }.to raise_error(CouchbaseOrm::Error::RecordInvalid) - - model.destroy - end - - it "should reload a model" do - model = BasicModel.new - - model.name = 'bob' - model.address = 'somewhere' - model.age = 34 - - expect(model.save).to be(true) - id = model.id - model.name = nil - expect(model.changed?).to be(true) - - model.reload - expect(model.changed?).to be(false) - expect(model.id).to eq(id) - - model.destroy - expect(model.destroyed?).to be(true) + model.name = 'bob' + model.age = 23 + expect(model.valid?).to be(true) + expect(model.save).to be(true) + + # Test update + model.name = nil + expect(model.valid?).to be(false) + expect(model.save).to be(false) + begin + model.save! + rescue ::CouchbaseOrm::Error::RecordInvalid => e + expect(e.record).to be(model) end - it "should update with action controler parameters" do - model = BasicModel.create! - params = ActionController::Parameters.new({ - name: 'Francesco', - age: 22, - foo: 'bar' + model.age = '23' # This value will be coerced + model.name = 'joe' + expect(model.valid?).to be(true) + expect(model.save!).to be(model) + + # coercion will fail here + model.age = 'a23' + expect{ model.save! }.to raise_error(CouchbaseOrm::Error::RecordInvalid) + + model.destroy + end + + it 'reloads a model' do + model = BasicModel.new + + model.name = 'bob' + model.address = 'somewhere' + model.age = 34 + + expect(model.save).to be(true) + id = model.id + model.name = nil + expect(model.changed?).to be(true) + + model.reload + expect(model.changed?).to be(false) + expect(model.id).to eq(id) + + model.destroy + expect(model.destroyed?).to be(true) + end + + it 'updates with action controler parameters' do + model = BasicModel.create! + params = ActionController::Parameters.new({ + name: 'Francesco', + age: 22, + foo: 'bar' + }) + model.update!(params.permit(:name, :age)) + model.reload + expect(model.age).to eq(22) + end + + it 'updates attributes' do + model = BasicModel.new + + model.update_attributes({ + name: 'bob', + age: 34 + }) + + expect(model.new_record?).to be(false) + expect(model.destroyed?).to be(false) + expect(model.persisted?).to be(true) + + expect(model.name).to eq('bob') + expect(model.age).to be(34) + expect(model.address).to be_nil + + model.destroy + expect(model.destroyed?).to be(true) + end + + it 'does not allow to update unkown attributes' do + model = BasicModel.new + + expect{ + model.update_attributes({ + name: 'bob', + age: 34, + foo: 'bar' + }) + }.to raise_error(ActiveModel::UnknownAttributeError) + end + + it 'does not allow to create with unkown attributes' do + expect{ + BasicModel.create({ + name: 'bob', + age: 34, + foo: 'bar' }) - model.update!(params.permit(:name, :age)) - model.reload - expect(model.age).to eq(22) - end - - it "should update attributes" do - model = BasicModel.new - - model.update_attributes({ - name: 'bob', - age: 34 - }) - - expect(model.new_record?).to be(false) - expect(model.destroyed?).to be(false) - expect(model.persisted?).to be(true) - - expect(model.name).to eq('bob') - expect(model.age).to be(34) - expect(model.address).to be(nil) - - model.destroy - expect(model.destroyed?).to be(true) - end - - it "should not allow to update unkown attributes" do - model = BasicModel.new - - expect{ model.update_attributes({ - name: 'bob', - age: 34, - foo: 'bar' - }) }.to raise_error(ActiveModel::UnknownAttributeError) - end - - it "should not allow to create with unkown attributes" do - expect{ BasicModel.create({ - name: 'bob', - age: 34, - foo: 'bar' - }) }.to raise_error(ActiveModel::UnknownAttributeError) - end - - it "should not allow to update with unkown attributes" do - model = BasicModel.create!(name: 'bob', age: 34) - expect{ model.update({ - foo: 'bar' - }) }.to raise_error(ActiveModel::UnknownAttributeError) - end - - it "should not perform validation with validate true" do - model = ModelWithValidations.new - - expect(model.valid?).to be(false) - expect(model.save(validate: false)).to be(true) - expect(model.persisted?).to be(true) - - model.destroy - end - - describe BasicModel do - it_behaves_like "ActiveModel" - end - - describe ModelWithDefaults do - it_behaves_like "ActiveModel" - end - - describe ModelWithCallbacks do - it_behaves_like "ActiveModel" - end - - describe ModelWithValidations do - it_behaves_like "ActiveModel" - end + }.to raise_error(ActiveModel::UnknownAttributeError) + end + + it 'does not allow to update with unkown attributes' do + model = BasicModel.create!(name: 'bob', age: 34) + expect{ + model.update({ + foo: 'bar' + }) + }.to raise_error(ActiveModel::UnknownAttributeError) + end + + it 'does not perform validation with validate true' do + model = ModelWithValidations.new + + expect(model.valid?).to be(false) + expect(model.save(validate: false)).to be(true) + expect(model.persisted?).to be(true) + + model.destroy + end + + describe BasicModel do + it_behaves_like 'ActiveModel' + end + + describe ModelWithDefaults do + it_behaves_like 'ActiveModel' + end + + describe ModelWithCallbacks do + it_behaves_like 'ActiveModel' + end + + describe ModelWithValidations do + it_behaves_like 'ActiveModel' + end end diff --git a/spec/relation_nested_spec.rb b/spec/relation_nested_spec.rb index 228dd15f..b8994e0b 100644 --- a/spec/relation_nested_spec.rb +++ b/spec/relation_nested_spec.rb @@ -1,75 +1,74 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) class NestedModel < CouchbaseOrm::NestedDocument - attribute :name, :string - attribute :size, :integer - attribute :child, :nested, type: NestedModel + attribute :name, :string + attribute :size, :integer + attribute :child, :nested, type: NestedModel end class RelationParentModel < CouchbaseOrm::Base - attribute :name, :string - attribute :sub, :nested, type: NestedModel - attribute :subs, :array, type: NestedModel + attribute :name, :string + attribute :sub, :nested, type: NestedModel + attribute :subs, :array, type: NestedModel end describe CouchbaseOrm::Relation do - before(:each) do - RelationParentModel.delete_all - CouchbaseOrm.logger.debug "Cleaned before tests" - end + before do + RelationParentModel.delete_all + CouchbaseOrm.logger.debug 'Cleaned before tests' + end - after(:all) do - CouchbaseOrm.logger.debug "Cleanup after all tests" - RelationParentModel.delete_all - end + after(:all) do + CouchbaseOrm.logger.debug 'Cleanup after all tests' + RelationParentModel.delete_all + end - it "should query on nested array attribute" do - RelationParentModel.create(name: "parent_without_subs") - parent = RelationParentModel.create(name: "parent") - parent.subs = [ - NestedModel.new(name: "sub2"), - NestedModel.new(name: "sub3") - ] - parent.save! + it 'queries on nested array attribute' do + RelationParentModel.create(name: 'parent_without_subs') + parent = RelationParentModel.create(name: 'parent') + parent.subs = [ + NestedModel.new(name: 'sub2'), + NestedModel.new(name: 'sub3') + ] + parent.save! - expect(RelationParentModel.where(subs: {name: 'sub2'}).first).to eq parent - expect(RelationParentModel.where(subs: {name: ['sub3', 'subX']}).first).to eq parent - end - - it "should query by gte function" do - parent = RelationParentModel.create(name: "parent") - parent.subs = [ - NestedModel.new(name: "sub2", size: 2), - NestedModel.new(name: "sub3", size: 3), - NestedModel.new(name: "sub4", size: 4) - ] - parent.save! - expect(RelationParentModel.where(subs: {size: {_gte: 3, _lt: 4}}).first).to eq parent - end + expect(RelationParentModel.where(subs: {name: 'sub2'}).first).to eq parent + expect(RelationParentModel.where(subs: {name: ['sub3', 'subX']}).first).to eq parent + end - it "should query by nested attribute" do - RelationParentModel.create(name: "parent_without_sub") - parent = RelationParentModel.create(name: "parent") - parent.sub = NestedModel.new(name: "sub") - parent.save! - expect(RelationParentModel.where('sub.name': 'sub').first).to eq parent - expect(RelationParentModel.where(sub: {name: 'sub'}).first).to eq parent - expect(RelationParentModel.where(sub: {name: ['sub', 'subX']}).first).to eq parent - expect(RelationParentModel.where(sub: {name: ['subX']}).first).to be_nil + it 'queries by gte function' do + parent = RelationParentModel.create(name: 'parent') + parent.subs = [ + NestedModel.new(name: 'sub2', size: 2), + NestedModel.new(name: 'sub3', size: 3), + NestedModel.new(name: 'sub4', size: 4) + ] + parent.save! + expect(RelationParentModel.where(subs: {size: {_gte: 3, _lt: 4}}).first).to eq parent + end - end + it 'queries by nested attribute' do + RelationParentModel.create(name: 'parent_without_sub') + parent = RelationParentModel.create(name: 'parent') + parent.sub = NestedModel.new(name: 'sub') + parent.save! + expect(RelationParentModel.where('sub.name': 'sub').first).to eq parent + expect(RelationParentModel.where(sub: {name: 'sub'}).first).to eq parent + expect(RelationParentModel.where(sub: {name: ['sub', 'subX']}).first).to eq parent + expect(RelationParentModel.where(sub: {name: ['subX']}).first).to be_nil + end - it "should query by grand child attribute" do - RelationParentModel.create(name: "parent_without_sub") - parent = RelationParentModel.create(name: "parent") - parent.sub = NestedModel.new(name: "sub", child: NestedModel.new(name: "child")) - parent.save! + it 'queries by grand child attribute' do + RelationParentModel.create(name: 'parent_without_sub') + parent = RelationParentModel.create(name: 'parent') + parent.sub = NestedModel.new(name: 'sub', child: NestedModel.new(name: 'child')) + parent.save! - expect(RelationParentModel.where(sub: {child: {name: 'child'}}).first).to eq parent - expect(RelationParentModel.where(sub: {child: {name: ['child', 'childX']}}).first).to eq parent - expect(RelationParentModel.where(sub: {child: {name: ['childX']}}).first).to be_nil - end + expect(RelationParentModel.where(sub: {child: {name: 'child'}}).first).to eq parent + expect(RelationParentModel.where(sub: {child: {name: ['child', 'childX']}}).first).to eq parent + expect(RelationParentModel.where(sub: {child: {name: ['childX']}}).first).to be_nil + end end - diff --git a/spec/relation_spec.rb b/spec/relation_spec.rb index f58c18ce..10e7ee4a 100644 --- a/spec/relation_spec.rb +++ b/spec/relation_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) class NestedRelationModel < CouchbaseOrm::NestedDocument attribute :name, :string @@ -8,423 +9,442 @@ class NestedRelationModel < CouchbaseOrm::NestedDocument end class PathRelationModel < CouchbaseOrm::NestedDocument - attribute :pathelement, :nested, type: PathRelationModel - attribute :children, :array, type: NestedRelationModel + attribute :pathelement, :nested, type: PathRelationModel + attribute :children, :array, type: NestedRelationModel end class RelationModel < CouchbaseOrm::Base - attribute :name, :string - attribute :last_name, :string - attribute :active, :boolean - attribute :age, :integer - attribute :children, :array, type: NestedRelationModel - attribute :pathelement, :nested, type: PathRelationModel - def self.adult - where(age: {_gte: 18}) - end - - def self.active - where(active: true) - end + attribute :name, :string + attribute :last_name, :string + attribute :active, :boolean + attribute :age, :integer + attribute :children, :array, type: NestedRelationModel + attribute :pathelement, :nested, type: PathRelationModel + def self.adult + where(age: {_gte: 18}) + end + + def self.active + where(active: true) + end end describe CouchbaseOrm::Relation do - before(:each) do - RelationModel.delete_all - CouchbaseOrm.logger.debug "Cleaned before tests" - end - - after(:all) do - CouchbaseOrm.logger.debug "Cleanup after all tests" - RelationModel.delete_all - end - - it "should return a relation" do - expect(RelationModel.all).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation) - end - - it "should query with conditions" do - RelationModel.create! name: :bob, active: true, age: 10 - RelationModel.create! name: :alice, active: true, age: 20 - RelationModel.create! name: :john, active: false, age: 30 - expect(RelationModel.where(active: true).count).to eq(2) - expect(RelationModel.where(active: true).size).to eq(2) - - expect(RelationModel.where(active: true).to_a.map(&:name)).to match_array(%w[bob alice]) - expect(RelationModel.where(active: true).where(age: 10).to_a.map(&:name)).to match_array(%w[bob]) - end - - it "should query with merged conditions" do - RelationModel.create! name: :bob, active: true, age: 10 - RelationModel.create! name: :bob, active: false, age: 10 - RelationModel.create! name: :alice, active: true, age: 20 - RelationModel.create! name: :john, active: false, age: 30 - - expect(RelationModel.where(active: true).where(name: 'bob').count).to eq(1) - end - - it "should find_by conditions" do - RelationModel.create! name: :bob, active: true, age: 10 - m = RelationModel.create! name: :bob, active: false, age: 10 - RelationModel.create! name: :alice, active: true, age: 20 - RelationModel.create! name: :alice, active: false, age: 20 - - expect(RelationModel.where(name: 'bob').find_by(active: false)).to eq(m) - expect(RelationModel.find_by(name: 'bob', active: false)).to eq(m) - end - - it "should count without loading models" do - RelationModel.create! name: :bob, active: true, age: 10 - RelationModel.create! name: :alice, active: false, age: 20 - - expect(RelationModel).not_to receive(:find) - - expect(RelationModel.where(active: true).count).to eq(1) - end - - it "Should delete_all" do - RelationModel.create! - RelationModel.create! - RelationModel.delete_all - expect(RelationModel.ids).to match_array([]) - end - - it "Should delete_all with conditions" do - RelationModel.create! - jane = RelationModel.create! name: "Jane" - RelationModel.where(name: nil).delete_all - expect(RelationModel.ids).to match_array([jane.id]) - end - - it "Should query ids" do - expect(RelationModel.ids).to match_array([]) - m1 = RelationModel.create! - m2 = RelationModel.create! - expect(RelationModel.ids).to match_array([m1.id, m2.id]) - end - - it "Should query ids with conditions" do - m1 = RelationModel.create!(active: true, name: "Jane") - _m2 = RelationModel.create!(active: false, name: "Bob" ) - _m3 = RelationModel.create!(active: false, name: "Jane") - expect(RelationModel.where(active: true, name: "Jane").ids).to match_array([m1.id]) - end - - it "Should query ids with conditions and limit" do - RelationModel.create!(active: true, name: "Jane", age: 2) - RelationModel.create!(active: false, name: "Bob", age: 3) - m = RelationModel.create!(active: true, name: "Jane", age: 1) - RelationModel.create!(active: false, name: "Jane", age: 0) - - expect(RelationModel.where(active: true, name: "Jane").order(:age).limit(1).ids).to match_array([m.id]) - expect(RelationModel.limit(1).where(active: true, name: "Jane").order(:age).ids).to match_array([m.id]) - end - - it "Should query ids with order" do - m1 = RelationModel.create!(age: 10, name: 'b') - m2 = RelationModel.create!(age: 20, name: 'a') - expect(RelationModel.order(age: :desc).ids).to match_array([m2.id, m1.id]) - expect(RelationModel.order(age: :asc).ids).to match_array([m1.id, m2.id]) - expect(RelationModel.order(name: :desc).ids).to match_array([m1.id, m2.id]) - expect(RelationModel.order(name: :asc).ids).to match_array([m2.id, m1.id]) - expect(RelationModel.order(:name).ids).to match_array([m2.id, m1.id]) - expect(RelationModel.order(:age).ids).to match_array([m1.id, m2.id]) - end - - it "Should query with list order" do - m1 = RelationModel.create!(age: 20, name: 'b') - m2 = RelationModel.create!(age: 5, name: 'a') - m3 = RelationModel.create!(age: 20, name: 'a') - expect(RelationModel.order(:age, :name).ids).to match_array([m2.id, m3.id, m1.id]) - end - - it "Should query with chained order" do - m1 = RelationModel.create!(age: 10, name: 'b') - m2 = RelationModel.create!(age: 20, name: 'a') - m3 = RelationModel.create!(age: 20, name: 'c') - expect(RelationModel.order(age: :desc).order(name: :asc).ids).to match_array([m2.id, m3.id, m1.id]) - end - - it "Should query with order chained with list" do - m1 = RelationModel.create!(age: 20, name: 'b') - m2 = RelationModel.create!(age: 5, name: 'a') - m3 = RelationModel.create!(age: 20, name: 'a', last_name: 'c') - m4 = RelationModel.create!(age: 20, name: 'a', last_name: 'a') - expect(RelationModel.order(:age, :name).order(:last_name).ids).to match_array([m2.id, m4.id, m3.id, m1.id]) - end - - it "Should query all" do - m1 = RelationModel.create!(active: true) - m2 = RelationModel.create!(active: false) - expect(RelationModel.all).to match_array([m1, m2]) - end - - it "should query all with condition and order" do - m1 = RelationModel.create!(active: true, age: 10) - m2 = RelationModel.create!(active: true, age: 20) - _m3 = RelationModel.create!(active: false, age: 30) - expect(RelationModel.where(active: true).order(age: :desc).all.to_a).to eq([m2, m1]) - expect(RelationModel.all.where(active: true).order(age: :asc).to_a).to eq([m1, m2]) - end - - it "should query by id" do - m1 = RelationModel.create!(active: true, age: 10) - m2 = RelationModel.create!(active: true, age: 20) - _m3 = RelationModel.create!(active: false, age: 30) - expect(RelationModel.where(id: [m1.id, m2.id])).to match_array([m1, m2]) - end - - it "should query first" do - _m1 = RelationModel.create!(active: true, age: 10) - m2 = RelationModel.create!(active: true, age: 20) - _m3 = RelationModel.create!(active: false, age: 30) - expect(RelationModel.where(active: true).order(age: :desc).first).to eq m2 - end - - it "should query array first" do - _m1 = RelationModel.create!(active: true, age: 10) - m2 = RelationModel.create!(active: true, age: 20) - _m3 = RelationModel.create!(active: false, age: 30) - expect(RelationModel.where(active: true).order(age: :desc)[0]).to eq m2 - end - - it "should query last" do - _m1 = RelationModel.create!(active: true, age: 10) - m2 = RelationModel.create!(active: true, age: 20) - _m3 = RelationModel.create!(active: false, age: 30) - expect(RelationModel.where(active: true).order(age: :asc).last).to eq m2 - end - - it "should return a relation when using not" do - expect(RelationModel.not(active: true)).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation) - expect(RelationModel.all.not(active: true)).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation) - end - - it "should have a to_ary method" do - expect(RelationModel.not(active: true)).to respond_to(:to_ary) - expect(RelationModel.all.not(active: true)).to respond_to(:to_ary) - end - - it "should have a each method" do - expect(RelationModel.not(active: true)).to respond_to(:each) - expect(RelationModel.all.not(active: true)).to respond_to(:each) - end - - it "should pluck one element" do - _m1 = RelationModel.create!(active: true, age: 10) - _m2 = RelationModel.create!(active: true, age: 20) - _m3 = RelationModel.create!(active: false, age: 30) - expect(RelationModel.order(:age).pluck(:age)).to match_array([10, 20, 30]) - end - - it "should find one element" do - _m1 = RelationModel.create!(active: true, age: 10) - m2 = RelationModel.create!(active: true, age: 20) - _m3 = RelationModel.create!(active: false, age: 30) - expect(RelationModel.all.find do |m| - m.age == 20 - end).to eq m2 - end - - it "should pluck several elements" do - _m1 = RelationModel.create!(active: true, age: 10) - _m2 = RelationModel.create!(active: true, age: 20) - _m3 = RelationModel.create!(active: false, age: 30) - expect(RelationModel.order(:age).pluck(:age, :active)).to match_array([[10, true], [20, true], [30, false]]) - end - - it "should query true boolean" do - m1 = RelationModel.create!(active: true) - _m2 = RelationModel.create!(active: false) - _m3 = RelationModel.create!(active: nil) - expect(RelationModel.where(active: true)).to match_array([m1]) - end - - it "should not query true boolean" do - _m1 = RelationModel.create!(active: true) - m2 = RelationModel.create!(active: false) - _m3 = RelationModel.create!(active: nil) - expect(RelationModel.not(active: true)).to match_array([m2]) # keep ActiveRecord compatibility by not returning _m3 - end - - it "should query false boolean" do - _m1 = RelationModel.create!(active: true) - m2 = RelationModel.create!(active: false) - _m3 = RelationModel.create!(active: nil) - expect(RelationModel.where(active: false)).to match_array([m2]) - end - - it "should not query false boolean" do - m1 = RelationModel.create!(active: true) - _m2 = RelationModel.create!(active: false) - _m3 = RelationModel.create!(active: nil) - expect(RelationModel.not(active: false)).to match_array([m1]) # keep ActiveRecord compatibility by not returning _m3 - end - - it "should query nil boolean" do - _m1 = RelationModel.create!(active: true) - _m2 = RelationModel.create!(active: false) - m3 = RelationModel.create!(active: nil) - expect(RelationModel.where(active: nil)).to match_array([m3]) - end - - it "should not query nil boolean" do - m1 = RelationModel.create!(active: true) - m2 = RelationModel.create!(active: false) - _m3 = RelationModel.create!(active: nil) - expect(RelationModel.not(active: nil)).to match_array([m1, m2]) - end - - it "should query nil and false boolean" do - _m1 = RelationModel.create!(active: true) - m2 = RelationModel.create!(active: false) - m3 = RelationModel.create!(active: nil) - expect(RelationModel.where(active: [false, nil])).to match_array([m2, m3]) - end - - it "should not query nil and false boolean" do - m1 = RelationModel.create!(active: true) - _m2 = RelationModel.create!(active: false) - _m3 = RelationModel.create!(active: nil) - expect(RelationModel.not(active: [false, nil])).to match_array([m1]) - end - - it "should query by string" do - m1 = RelationModel.create!(age: 20, active: true) - m2 = RelationModel.create!(age: 10, active: false) - m3 = RelationModel.create!(age: 20, active: false) - - expect(RelationModel.where("active = true").count).to eq(1) - expect(RelationModel.where("active = true")).to match_array([m1]) - expect(RelationModel.where("active = false")).to match_array([m2, m3]) - expect(RelationModel.where(age: 20).where("active = false")).to match_array([m3]) - expect(RelationModel.where("active = false").where(age: 20)).to match_array([m3]) - end - - it "is empty" do - expect(RelationModel.empty?).to eq(true) - end - - it "is not empty with a created model" do - RelationModel.create!(active: true) - expect(RelationModel.empty?).to eq(false) - end - - describe "operators" do - it "should query by gte and lte" do - _m1 = RelationModel.create!(age: 10) - m2 = RelationModel.create!(age: 20) - m3 = RelationModel.create!(age: 30) - _m4 = RelationModel.create!(age: 40) - expect(RelationModel.where(age: {_lte: 30, _gt:10})).to match_array([m2, m3]) - end - end - - describe "update_all" do - it "should update matching documents" do - m1 = RelationModel.create!(age: 10) - m2 = RelationModel.create!(age: 20) - m3 = RelationModel.create!(age: 30) - m4 = RelationModel.create!(age: 40) - RelationModel.where(age: {_lte: 30, _gt:10}).update_all(age: 50) - expect(m1.reload.age).to eq(10) - expect(m2.reload.age).to eq(50) - expect(m3.reload.age).to eq(50) - expect(m4.reload.age).to eq(40) + before do + RelationModel.delete_all + CouchbaseOrm.logger.debug 'Cleaned before tests' + end + + after(:all) do + CouchbaseOrm.logger.debug 'Cleanup after all tests' + RelationModel.delete_all + end + + it 'returns a relation' do + expect(RelationModel.all).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation) + end + + it 'queries with conditions' do + RelationModel.create! name: :bob, active: true, age: 10 + RelationModel.create! name: :alice, active: true, age: 20 + RelationModel.create! name: :john, active: false, age: 30 + expect(RelationModel.where(active: true).count).to eq(2) + expect(RelationModel.where(active: true).size).to eq(2) + + expect(RelationModel.where(active: true).to_a.map(&:name)).to match_array(%w[bob alice]) + expect(RelationModel.where(active: true).where(age: 10).to_a.map(&:name)).to match_array(%w[bob]) + end + + it 'queries with merged conditions' do + RelationModel.create! name: :bob, active: true, age: 10 + RelationModel.create! name: :bob, active: false, age: 10 + RelationModel.create! name: :alice, active: true, age: 20 + RelationModel.create! name: :john, active: false, age: 30 + + expect(RelationModel.where(active: true).where(name: 'bob').count).to eq(1) + end + + it 'find_bies conditions' do + RelationModel.create! name: :bob, active: true, age: 10 + m = RelationModel.create! name: :bob, active: false, age: 10 + RelationModel.create! name: :alice, active: true, age: 20 + RelationModel.create! name: :alice, active: false, age: 20 + + expect(RelationModel.where(name: 'bob').find_by(active: false)).to eq(m) + expect(RelationModel.find_by(name: 'bob', active: false)).to eq(m) + end + + it 'counts without loading models' do + RelationModel.create! name: :bob, active: true, age: 10 + RelationModel.create! name: :alice, active: false, age: 20 + + expect(RelationModel).not_to receive(:find) + + expect(RelationModel.where(active: true).count).to eq(1) + end + + it 'delete_alls' do + RelationModel.create! + RelationModel.create! + RelationModel.delete_all + expect(RelationModel.ids).to be_empty + end + + it 'delete_alls with conditions' do + RelationModel.create! + jane = RelationModel.create! name: 'Jane' + RelationModel.where(name: nil).delete_all + expect(RelationModel.ids).to contain_exactly(jane.id) + end + + it 'queries ids' do + expect(RelationModel.ids).to be_empty + m1 = RelationModel.create! + m2 = RelationModel.create! + expect(RelationModel.ids).to contain_exactly(m1.id, m2.id) + end + + it 'queries ids with conditions' do + m1 = RelationModel.create!(active: true, name: 'Jane') + _m2 = RelationModel.create!(active: false, name: 'Bob') + _m3 = RelationModel.create!(active: false, name: 'Jane') + expect(RelationModel.where(active: true, name: 'Jane').ids).to contain_exactly(m1.id) + end + + it 'queries ids with conditions and limit' do + RelationModel.create!(active: true, name: 'Jane', age: 2) + RelationModel.create!(active: false, name: 'Bob', age: 3) + m = RelationModel.create!(active: true, name: 'Jane', age: 1) + RelationModel.create!(active: false, name: 'Jane', age: 0) + + expect(RelationModel.where(active: true, name: 'Jane').order(:age).limit(1).ids).to contain_exactly(m.id) + expect(RelationModel.limit(1).where(active: true, name: 'Jane').order(:age).ids).to contain_exactly(m.id) + end + + it 'queries ids with order' do + m1 = RelationModel.create!(age: 10, name: 'b') + m2 = RelationModel.create!(age: 20, name: 'a') + expect(RelationModel.order(age: :desc).ids).to contain_exactly(m2.id, m1.id) + expect(RelationModel.order(age: :asc).ids).to contain_exactly(m1.id, m2.id) + expect(RelationModel.order(name: :desc).ids).to contain_exactly(m1.id, m2.id) + expect(RelationModel.order(name: :asc).ids).to contain_exactly(m2.id, m1.id) + expect(RelationModel.order(:name).ids).to contain_exactly(m2.id, m1.id) + expect(RelationModel.order(:age).ids).to contain_exactly(m1.id, m2.id) + end + + it 'queries with list order' do + m1 = RelationModel.create!(age: 20, name: 'b') + m2 = RelationModel.create!(age: 5, name: 'a') + m3 = RelationModel.create!(age: 20, name: 'a') + expect(RelationModel.order(:age, :name).ids).to contain_exactly(m2.id, m3.id, m1.id) + end + + it 'queries with chained order' do + m1 = RelationModel.create!(age: 10, name: 'b') + m2 = RelationModel.create!(age: 20, name: 'a') + m3 = RelationModel.create!(age: 20, name: 'c') + expect(RelationModel.order(age: :desc).order(name: :asc).ids).to contain_exactly(m2.id, m3.id, m1.id) + end + + it 'queries with order chained with list' do + m1 = RelationModel.create!(age: 20, name: 'b') + m2 = RelationModel.create!(age: 5, name: 'a') + m3 = RelationModel.create!(age: 20, name: 'a', last_name: 'c') + m4 = RelationModel.create!(age: 20, name: 'a', last_name: 'a') + expect(RelationModel.order(:age, :name).order(:last_name).ids).to contain_exactly(m2.id, m4.id, m3.id, m1.id) + end + + it 'queries all' do + m1 = RelationModel.create!(active: true) + m2 = RelationModel.create!(active: false) + expect(RelationModel.all).to contain_exactly(m1, m2) + end + + it 'queries all with condition and order' do + m1 = RelationModel.create!(active: true, age: 10) + m2 = RelationModel.create!(active: true, age: 20) + _m3 = RelationModel.create!(active: false, age: 30) + expect(RelationModel.where(active: true).order(age: :desc).all.to_a).to eq([m2, m1]) + expect(RelationModel.all.where(active: true).order(age: :asc).to_a).to eq([m1, m2]) + end + + it 'queries by id' do + m1 = RelationModel.create!(active: true, age: 10) + m2 = RelationModel.create!(active: true, age: 20) + _m3 = RelationModel.create!(active: false, age: 30) + expect(RelationModel.where(id: [m1.id, m2.id])).to contain_exactly(m1, m2) + end + + it 'queries first' do + _m1 = RelationModel.create!(active: true, age: 10) + m2 = RelationModel.create!(active: true, age: 20) + _m3 = RelationModel.create!(active: false, age: 30) + expect(RelationModel.where(active: true).order(age: :desc).first).to eq m2 + end + + it 'queries array first' do + _m1 = RelationModel.create!(active: true, age: 10) + m2 = RelationModel.create!(active: true, age: 20) + _m3 = RelationModel.create!(active: false, age: 30) + expect(RelationModel.where(active: true).order(age: :desc)[0]).to eq m2 + end + + it 'queries last' do + _m1 = RelationModel.create!(active: true, age: 10) + m2 = RelationModel.create!(active: true, age: 20) + _m3 = RelationModel.create!(active: false, age: 30) + expect(RelationModel.where(active: true).order(age: :asc).last).to eq m2 + end + + it 'returns a relation when using not' do + expect(RelationModel.not(active: true)).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation) + expect(RelationModel.all.not(active: true)).to be_a(CouchbaseOrm::Relation::CouchbaseOrm_Relation) + end + + it 'has a to_ary method' do + expect(RelationModel.not(active: true)).to respond_to(:to_ary) + expect(RelationModel.all.not(active: true)).to respond_to(:to_ary) + end + + it 'has a each method' do + expect(RelationModel.not(active: true)).to respond_to(:each) + expect(RelationModel.all.not(active: true)).to respond_to(:each) + end + + it 'plucks one element' do + _m1 = RelationModel.create!(active: true, age: 10) + _m2 = RelationModel.create!(active: true, age: 20) + _m3 = RelationModel.create!(active: false, age: 30) + expect(RelationModel.order(:age).pluck(:age)).to contain_exactly(10, 20, 30) + end + + it 'finds one element' do + _m1 = RelationModel.create!(active: true, age: 10) + m2 = RelationModel.create!(active: true, age: 20) + _m3 = RelationModel.create!(active: false, age: 30) + expect(RelationModel.all.find do |m| + m.age == 20 + end).to eq m2 + end + + it 'plucks several elements' do + _m1 = RelationModel.create!(active: true, age: 10) + _m2 = RelationModel.create!(active: true, age: 20) + _m3 = RelationModel.create!(active: false, age: 30) + expect(RelationModel.order(:age).pluck(:age, :active)).to contain_exactly([10, true], [20, true], [30, false]) + end + + it 'queries true boolean' do + m1 = RelationModel.create!(active: true) + _m2 = RelationModel.create!(active: false) + _m3 = RelationModel.create!(active: nil) + expect(RelationModel.where(active: true)).to contain_exactly(m1) + end + + it 'does not query true boolean' do + _m1 = RelationModel.create!(active: true) + m2 = RelationModel.create!(active: false) + _m3 = RelationModel.create!(active: nil) + expect(RelationModel.not(active: true)).to contain_exactly(m2) # keep ActiveRecord compatibility by not returning _m3 + end + + it 'queries false boolean' do + _m1 = RelationModel.create!(active: true) + m2 = RelationModel.create!(active: false) + _m3 = RelationModel.create!(active: nil) + expect(RelationModel.where(active: false)).to contain_exactly(m2) + end + + it 'does not query false boolean' do + m1 = RelationModel.create!(active: true) + _m2 = RelationModel.create!(active: false) + _m3 = RelationModel.create!(active: nil) + expect(RelationModel.not(active: false)).to contain_exactly(m1) # keep ActiveRecord compatibility by not returning _m3 + end + + it 'queries nil boolean' do + _m1 = RelationModel.create!(active: true) + _m2 = RelationModel.create!(active: false) + m3 = RelationModel.create!(active: nil) + expect(RelationModel.where(active: nil)).to contain_exactly(m3) + end + + it 'does not query nil boolean' do + m1 = RelationModel.create!(active: true) + m2 = RelationModel.create!(active: false) + _m3 = RelationModel.create!(active: nil) + expect(RelationModel.not(active: nil)).to contain_exactly(m1, m2) + end + + it 'queries nil and false boolean' do + _m1 = RelationModel.create!(active: true) + m2 = RelationModel.create!(active: false) + m3 = RelationModel.create!(active: nil) + expect(RelationModel.where(active: [false, nil])).to contain_exactly(m2, m3) + end + + it 'does not query nil and false boolean' do + m1 = RelationModel.create!(active: true) + _m2 = RelationModel.create!(active: false) + _m3 = RelationModel.create!(active: nil) + expect(RelationModel.not(active: [false, nil])).to contain_exactly(m1) + end + + it 'queries by string' do + m1 = RelationModel.create!(age: 20, active: true) + m2 = RelationModel.create!(age: 10, active: false) + m3 = RelationModel.create!(age: 20, active: false) + + expect(RelationModel.where('active = true').count).to eq(1) + expect(RelationModel.where('active = true')).to contain_exactly(m1) + expect(RelationModel.where('active = false')).to contain_exactly(m2, m3) + expect(RelationModel.where(age: 20).where('active = false')).to contain_exactly(m3) + expect(RelationModel.where('active = false').where(age: 20)).to contain_exactly(m3) + end + + it 'is empty' do + expect(RelationModel.empty?).to eq(true) + end + + it 'is not empty with a created model' do + RelationModel.create!(active: true) + expect(RelationModel.empty?).to eq(false) + end + + describe 'operators' do + it 'queries by gte and lte' do + _m1 = RelationModel.create!(age: 10) + m2 = RelationModel.create!(age: 20) + m3 = RelationModel.create!(age: 30) + _m4 = RelationModel.create!(age: 40) + expect(RelationModel.where(age: {_lte: 30, _gt: 10})).to contain_exactly(m2, m3) + end + end + + describe 'update_all' do + it 'updates matching documents' do + m1 = RelationModel.create!(age: 10) + m2 = RelationModel.create!(age: 20) + m3 = RelationModel.create!(age: 30) + m4 = RelationModel.create!(age: 40) + RelationModel.where(age: {_lte: 30, _gt: 10}).update_all(age: 50) + expect(m1.reload.age).to eq(10) + expect(m2.reload.age).to eq(50) + expect(m3.reload.age).to eq(50) + expect(m4.reload.age).to eq(40) + end + + it 'updates nested attributes with a for clause (when hash style)' do + m1 = RelationModel.create!(age: 10, + children: [ +NestedRelationModel.new(age: 10, name: 'Tom'), NestedRelationModel.new(age: 20, name: 'Jerry') +]) + m2 = RelationModel.create!(age: 20, + children: [ +NestedRelationModel.new(age: 15, name: 'Tom'), NestedRelationModel.new(age: 20, name: 'Jerry') +]) + m3 = RelationModel.create!(age: 20, + children: [ +NestedRelationModel.new(age: 10, name: 'Tom'), NestedRelationModel.new(age: 20, name: 'Jerry') +]) + + RelationModel.where(age: 20).update_all(child: {age: 50, _for: :children, _when: {child: {name: 'Tom'}}}) + + expect(m1.reload.children.map(&:age)).to eq([10, 20]) + expect(m2.reload.children.map(&:age)).to eq([50, 20]) + expect(m3.reload.children.map(&:age)).to eq([50, 20]) + end + + it 'updates nested attributes with a for clause (when path style)' do + m1 = RelationModel.create!(age: 10, + children: [ +NestedRelationModel.new(age: 10, name: 'Tom'), NestedRelationModel.new(age: 20, name: 'Jerry') +]) + m2 = RelationModel.create!(age: 20, + children: [ +NestedRelationModel.new(age: 15, name: 'Tom'), NestedRelationModel.new(age: 20, name: 'Jerry') +]) + m3 = RelationModel.create!(age: 20, + children: [ +NestedRelationModel.new(age: 10, name: 'Tom'), NestedRelationModel.new(age: 20, name: 'Jerry') +]) + + RelationModel.where(age: 20).update_all(child: {age: 50, _for: :children, _when: {'child.name': 'Tom'}}) + + expect(m1.reload.children.map(&:age)).to eq([10, 20]) + expect(m2.reload.children.map(&:age)).to eq([50, 20]) + expect(m3.reload.children.map(&:age)).to eq([50, 20]) + end + + it 'updates nested attributes with a path in a for clause' do + m1 = RelationModel.create!( + pathelement: PathRelationModel.new( + pathelement: PathRelationModel.new( + children: [NestedRelationModel.new(age: 10, name: 'Tom'), NestedRelationModel.new(age: 20, name: 'Jerry')] + ) + ) + ) + + RelationModel.update_all(child: {age: 50, _for: 'pathelement.pathelement.children', _when: {'child.name': 'Tom'}}) + + expect(m1.reload.pathelement.pathelement.children.map(&:age)).to eq([50, 20]) + end + end + + describe 'scopes' do + it 'returns block value' do + RelationModel.create!(active: true) + RelationModel.create!(active: false) + count = RelationModel.active.scoping do + RelationModel.count + end + expect(count).to eq 1 + end + + it 'chains scopes' do + _m1 = RelationModel.create!(age: 10, active: true) + _m2 = RelationModel.create!(age: 20, active: false) + m3 = RelationModel.create!(age: 30, active: true) + m4 = RelationModel.create!(age: 40, active: true) + + expect(RelationModel.all.adult.all.active.all).to contain_exactly(m3, m4) + expect(RelationModel.where(active: true).adult).to contain_exactly(m3, m4) + end + + it 'is scoped only in current thread' do + m1 = RelationModel.create!(active: true) + m2 = RelationModel.create!(active: false) + RelationModel.active.scoping do + expect(RelationModel.all).to contain_exactly(m1) + Thread.start do + expect(RelationModel.all).to contain_exactly(m1, m2) + end.join + end + end + + it 'propagates error' do + expect{ + RelationModel.active.scoping do + raise 'error' end - - it "should update nested attributes with a for clause (when hash style)" do - m1 = RelationModel.create!(age: 10, children: [NestedRelationModel.new(age: 10, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) - m2 = RelationModel.create!(age: 20, children: [NestedRelationModel.new(age: 15, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) - m3 = RelationModel.create!(age: 20, children: [NestedRelationModel.new(age: 10, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) - - RelationModel.where(age: 20).update_all(child: {age: 50, _for: :children, _when: {child: {name: "Tom"}}}) - - expect(m1.reload.children.map(&:age)).to eq([10, 20]) - expect(m2.reload.children.map(&:age)).to eq([50, 20]) - expect(m3.reload.children.map(&:age)).to eq([50, 20]) - end - - it "should update nested attributes with a for clause (when path style)" do - m1 = RelationModel.create!(age: 10, children: [NestedRelationModel.new(age: 10, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) - m2 = RelationModel.create!(age: 20, children: [NestedRelationModel.new(age: 15, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) - m3 = RelationModel.create!(age: 20, children: [NestedRelationModel.new(age: 10, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) - - RelationModel.where(age: 20).update_all(child: {age: 50, _for: :children, _when: {'child.name': "Tom"}}) - - expect(m1.reload.children.map(&:age)).to eq([10, 20]) - expect(m2.reload.children.map(&:age)).to eq([50, 20]) - expect(m3.reload.children.map(&:age)).to eq([50, 20]) - end - - it "should update nested attributes with a path in a for clause" do - m1 = RelationModel.create!( - pathelement: PathRelationModel.new( - pathelement: PathRelationModel.new( - children: [NestedRelationModel.new(age: 10, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")] - ) - ) - ) - - RelationModel.update_all(child: {age: 50, _for: 'pathelement.pathelement.children', _when: {'child.name': "Tom"}}) - - expect(m1.reload.pathelement.pathelement.children.map(&:age)).to eq([50, 20]) - end - end - - describe "scopes" do - it "should return block value" do - RelationModel.create!(active: true) - RelationModel.create!(active: false) - count = RelationModel.active.scoping do - RelationModel.count - end - expect(count).to eq 1 - end - - it "should chain scopes" do - _m1 = RelationModel.create!(age: 10, active: true) - _m2 = RelationModel.create!(age: 20, active: false) - m3 = RelationModel.create!(age: 30, active: true) - m4 = RelationModel.create!(age: 40, active: true) - - expect(RelationModel.all.adult.all.active.all).to match_array([m3, m4]) - expect(RelationModel.where(active: true).adult).to match_array([m3, m4]) - end - - it "should be scoped only in current thread" do - m1 = RelationModel.create!(active: true) - m2 = RelationModel.create!(active: false) - RelationModel.active.scoping do - expect(RelationModel.all).to match_array([m1]) - Thread.start do - expect(RelationModel.all).to match_array([m1, m2]) - end.join - end - end - - it "should propagate error" do - expect{RelationModel.active.scoping do - raise "error" - end}.to raise_error(RuntimeError) - end - - it "should not keep scope in case of error" do - _m1 = RelationModel.create!(age: 10, active: true) - _m2 = RelationModel.create!(age: 10, active: false) - _m3 = RelationModel.create!(age: 30, active: true) - _m3 = RelationModel.create!(age: 30, active: false) - RelationModel.active.scoping do - expect(RelationModel.count).to eq 2 - begin - RelationModel.adult.scoping do - raise "error" - end - rescue RuntimeError - end - expect(RelationModel.count).to eq 2 - end + }.to raise_error(RuntimeError) + end + + it 'does not keep scope in case of error' do + _m1 = RelationModel.create!(age: 10, active: true) + _m2 = RelationModel.create!(age: 10, active: false) + _m3 = RelationModel.create!(age: 30, active: true) + _m3 = RelationModel.create!(age: 30, active: false) + RelationModel.active.scoping do + expect(RelationModel.count).to eq 2 + begin + RelationModel.adult.scoping do + raise 'error' + end + rescue RuntimeError end + expect(RelationModel.count).to eq 2 + end end + end end - diff --git a/spec/support.rb b/spec/support.rb index 8a73443a..b2114972 100644 --- a/spec/support.rb +++ b/spec/support.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true + require 'simplecov' require 'couchbase-orm' require 'minitest/assertions' @@ -7,36 +9,36 @@ require 'pry-stack_explorer' 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 + add_group 'Core', [%r{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 -if ENV["COUCHBASE_FLUSH"] - CouchbaseOrm.logger.warn "Flushing Couchbase bucket '#{CouchbaseOrm::Connection.bucket.name}'" - CouchbaseOrm::Connection.cluster.buckets.flush_bucket(CouchbaseOrm::Connection.bucket.name) - raise "BucketFlushed" +if ENV['COUCHBASE_FLUSH'] + CouchbaseOrm.logger.warn "Flushing Couchbase bucket '#{CouchbaseOrm::Connection.bucket.name}'" + CouchbaseOrm::Connection.cluster.buckets.flush_bucket(CouchbaseOrm::Connection.bucket.name) + raise 'BucketFlushed' end -shared_examples_for "ActiveModel" do - include Minitest::Assertions - include ActiveModel::Lint::Tests +shared_examples_for 'ActiveModel' do + include Minitest::Assertions + include ActiveModel::Lint::Tests - def assertions - @__assertions__ ||= 0 - end + def assertions + @__assertions__ ||= 0 + end - def assertions=(val) - @__assertions__ = val - end + def assertions=(val) + @__assertions__ = val + end - ActiveModel::Lint::Tests.public_instance_methods.map { |method| method.to_s }.grep(/^test/).each do |method| - example(method.gsub('_', ' ')) { send method } - end + ActiveModel::Lint::Tests.public_instance_methods.map(&:to_s).grep(/^test/).each do |method| + example(method.tr('_', ' ')) { send method } + end - before do - @model = subject - end + before do + @model = subject + end end diff --git a/spec/type_array_spec.rb b/spec/type_array_spec.rb index f6bc8c6b..589d595d 100644 --- a/spec/type_array_spec.rb +++ b/spec/type_array_spec.rb @@ -1,52 +1,54 @@ -require File.expand_path("../support", __FILE__) +# frozen_string_literal: true -require "active_model" +require File.expand_path('support', __dir__) + +require 'active_model' class TypeArrayTest < CouchbaseOrm::Base - attribute :name - attribute :tags, :array, type: :string - attribute :milestones, :array, type: :date - attribute :flags, :array, type: :boolean - attribute :things + attribute :name + attribute :tags, :array, type: :string + attribute :milestones, :array, type: :date + attribute :flags, :array, type: :boolean + attribute :things end describe CouchbaseOrm::Base do - it "should be able to store and retrieve an array of strings" do - obj = TypeArrayTest.new - obj.tags = ["foo", "bar"] - obj.save! - - obj = TypeArrayTest.find(obj.id) - expect(obj.tags).to eq ["foo", "bar"] - end - - it "should be able to store and retrieve an array of date" do - dates = [Date.today, Date.today + 1] - obj = TypeArrayTest.new - obj.milestones = dates - obj.save! - - obj = TypeArrayTest.find(obj.id) - expect(obj.milestones).to eq dates - end - - it "should be able to store and retrieve an array of boolean" do - flags = [true, false] - obj = TypeArrayTest.new - obj.flags = flags - obj.save! - - obj = TypeArrayTest.find(obj.id) - expect(obj.flags).to eq flags - end - - it "should be able to store and retrieve an array of basic objects" do - things = [1, "1234", {"key" => 4}] - obj = TypeArrayTest.new - obj.things = things - obj.save! - - obj = TypeArrayTest.find(obj.id) - expect(obj.things).to eq things - end + it 'is able to store and retrieve an array of strings' do + obj = TypeArrayTest.new + obj.tags = ['foo', 'bar'] + obj.save! + + obj = TypeArrayTest.find(obj.id) + expect(obj.tags).to eq ['foo', 'bar'] + end + + it 'is able to store and retrieve an array of date' do + dates = [Date.today, Date.today + 1] + obj = TypeArrayTest.new + obj.milestones = dates + obj.save! + + obj = TypeArrayTest.find(obj.id) + expect(obj.milestones).to eq dates + end + + it 'is able to store and retrieve an array of boolean' do + flags = [true, false] + obj = TypeArrayTest.new + obj.flags = flags + obj.save! + + obj = TypeArrayTest.find(obj.id) + expect(obj.flags).to eq flags + end + + it 'is able to store and retrieve an array of basic objects' do + things = [1, '1234', {'key' => 4}] + obj = TypeArrayTest.new + obj.things = things + obj.save! + + obj = TypeArrayTest.find(obj.id) + expect(obj.things).to eq things + end end diff --git a/spec/type_encrypted_spec.rb b/spec/type_encrypted_spec.rb index b2fc8335..898062de 100644 --- a/spec/type_encrypted_spec.rb +++ b/spec/type_encrypted_spec.rb @@ -1,114 +1,120 @@ -require File.expand_path("../support", __FILE__) +# frozen_string_literal: true + +require File.expand_path('support', __dir__) require 'active_model' require 'base64' class SubTypeEncryptedTest < CouchbaseOrm::NestedDocument - attribute :name, :string - attribute :secret, :encrypted - attribute :secret2, :encrypted + attribute :name, :string + attribute :secret, :encrypted + attribute :secret2, :encrypted end class TypeEncryptedTest < CouchbaseOrm::Base - attribute :main, :nested, type: SubTypeEncryptedTest - attribute :others, :array, type: SubTypeEncryptedTest - attribute :secret, :encrypted - attribute :secret2, :encrypted + attribute :main, :nested, type: SubTypeEncryptedTest + attribute :others, :array, type: SubTypeEncryptedTest + attribute :secret, :encrypted + attribute :secret2, :encrypted end class SpecificAlgoTest < CouchbaseOrm::Base - attribute :secret, :encrypted, alg: "3DES" + attribute :secret, :encrypted, alg: '3DES' end describe CouchbaseOrm::Types::Encrypted do # Generated with SecureRandom.bytes(256) - let(:the_secret) { "\x17`\x1F\xEE\el\xE0\x9F<\x94\xFE\x8A.\x1A\x92\xB9\xC3@\x86\x9Cp\xBEl\x86\x0E\x8CJ\tB\x97*U)\x96\x06\xA3\xE9\x84\xA6xW%\xDCT\x8C^\xEA\t\xC7\xD8\xFC\xF1\xD3\xD3\xE2\xEA\x89\xCBuUs\xB3\xFF'W>\xDE\x9CP\xA9\xDE%\xA2\xDE\x11\xFD\b\x9C\xD4\x87J,\x91\x02f\x16R\xDE\x908\x05\x1C\xF9\xDF{\x0F\xB3e\xB2\xB2\x96\xD7\xCC\x16As\xD3I\x02w\xE0\x8FL\xC6S\xEFP\xAC\x15\e^\xC4!\x15\"KF1\x17\x06\xA0N\x00\x18\xBA\x87\xEA?H\xD4<\xB5\xBCV\xB50\fc\xC9F\"\xF0B\eg%\x8E\x88\xD0\x9Bc\xE4\x93\t\x98\xC8\x87\xCB4]\xD9K\xA3\xDF\x13Q\xC0T\xCA\x91;\b\x9Cp\xE0\x7FR h\xDA\xB7\xD5\x869\f\xCA\x80\x802\x19\x19\xDD\x9DO\xAE}\xCA\eX\xA3\xA8\xBE\xE1\xBCW0g\x19@5n\r\xD8\xF3\x05\x7F4\x9CI\x1F3\xC0\xBDQJyG\v\xED!s\xD5\xD0&\xC1\x1A\xBC\x17\xFD\x9Cd\xB5\xAF\xB6U\x8A" } - let(:base64_secret) { Base64.strict_encode64(the_secret) } - - it "prefix attribute on serialization" do - obj = TypeEncryptedTest.new(secret: base64_secret, secret2: "a secret") - expect_serialized_secret(obj) - end - - it "prefix attribute on nested objects" do - obj = TypeEncryptedTest.new(main: SubTypeEncryptedTest.new(secret: base64_secret, secret2: "a secret")) - expect_serialized_secret(obj.main) - end - - it "prefix attribute on array objects" do - obj = TypeEncryptedTest.new(others: [SubTypeEncryptedTest.new(secret: base64_secret, secret2: "a secret")]) - expect_serialized_secret(obj.others.first) - end - - def expect_serialized_secret(obj) - expect(obj.send(:serialized_attributes)["encrypted$secret"]).to eq({alg:"CB_MOBILE_CUSTOM", ciphertext: base64_secret}) - expect(obj.send(:serialized_attributes)).to_not have_key "secret" - expect(obj.send(:serialized_attributes)["encrypted$secret2"]).to eq({alg:"CB_MOBILE_CUSTOM", ciphertext: "a secret"}) - expect(obj.send(:serialized_attributes)).to_not have_key "secret2" - expect(JSON.parse(obj.to_json)["secret"]).to eq base64_secret - expect(obj.as_json["secret"]).to eq base64_secret - expect(obj.as_json["secret2"]).to eq "a secret" - end - - it "prefix with custom algo" do - obj = SpecificAlgoTest.new(secret: base64_secret) - expect(obj.send(:serialized_attributes)["encrypted$secret"]).to eq({alg:"3DES", ciphertext: base64_secret}) - expect(obj.send(:serialized_attributes)).to_not include "secret" - end - - it "decode encrypted attribute at reload" do - obj = TypeEncryptedTest.create!( - secret: base64_secret, - ) - obj.save! - obj.reload - expect(obj.secret).to eq base64_secret - end - - it "decode nested encrypted attribute at reload" do - obj = TypeEncryptedTest.create!( - main: SubTypeEncryptedTest.new(secret: base64_secret), - ) - obj.save! - obj.reload - expect(obj.main.secret).to eq base64_secret - end - - it "decode array encrypted attribute at reload" do - obj = TypeEncryptedTest.create!( - others: [SubTypeEncryptedTest.new(secret: base64_secret)] - ) - obj.save! - obj.reload - expect(obj.others.first.secret).to eq base64_secret - end - - it "decode encrypted attribute at load" do - obj = TypeEncryptedTest.create!( - secret: base64_secret, - ) - obj.save! - obj = TypeEncryptedTest.find(obj.id) - expect(obj.secret).to eq base64_secret - end - - it "decode nested encrypted attribute at load" do - obj = TypeEncryptedTest.create!( - main: SubTypeEncryptedTest.new(secret: base64_secret), - ) - obj.save! - obj = TypeEncryptedTest.find(obj.id) - - expect(obj.main.secret).to eq base64_secret - end - - it "decode array encrypted attribute at load" do - obj = TypeEncryptedTest.create!( - others: [SubTypeEncryptedTest.new(secret: base64_secret)] - ) - obj.save! - obj = TypeEncryptedTest.find(obj.id) - - expect(obj.others.first.secret).to eq base64_secret - end + let(:the_secret) { + "\x17`\x1F\xEE\el\xE0\x9F<\x94\xFE\x8A.\x1A\x92\xB9\xC3@\x86\x9Cp\xBEl\x86\x0E\x8CJ\tB\x97*U)\x96\x06\xA3\xE9\x84\xA6xW%\xDCT\x8C^\xEA\t\xC7\xD8\xFC\xF1\xD3\xD3\xE2\xEA\x89\xCBuUs\xB3\xFF'W>\xDE\x9CP\xA9\xDE%\xA2\xDE\x11\xFD\b\x9C\xD4\x87J,\x91\x02f\x16R\xDE\x908\x05\x1C\xF9\xDF{\x0F\xB3e\xB2\xB2\x96\xD7\xCC\x16As\xD3I\x02w\xE0\x8FL\xC6S\xEFP\xAC\x15\e^\xC4!\x15\"KF1\x17\x06\xA0N\x00\x18\xBA\x87\xEA?H\xD4<\xB5\xBCV\xB50\fc\xC9F\"\xF0B\eg%\x8E\x88\xD0\x9Bc\xE4\x93\t\x98\xC8\x87\xCB4]\xD9K\xA3\xDF\x13Q\xC0T\xCA\x91;\b\x9Cp\xE0\x7FR h\xDA\xB7\xD5\x869\f\xCA\x80\x802\x19\x19\xDD\x9DO\xAE}\xCA\eX\xA3\xA8\xBE\xE1\xBCW0g\x19@5n\r\xD8\xF3\x05\x7F4\x9CI\x1F3\xC0\xBDQJyG\v\xED!s\xD5\xD0&\xC1\x1A\xBC\x17\xFD\x9Cd\xB5\xAF\xB6U\x8A" + } + let(:base64_secret) { Base64.strict_encode64(the_secret) } + + it 'prefix attribute on serialization' do + obj = TypeEncryptedTest.new(secret: base64_secret, secret2: 'a secret') + expect_serialized_secret(obj) + end + + it 'prefix attribute on nested objects' do + obj = TypeEncryptedTest.new(main: SubTypeEncryptedTest.new(secret: base64_secret, secret2: 'a secret')) + expect_serialized_secret(obj.main) + end + + it 'prefix attribute on array objects' do + obj = TypeEncryptedTest.new(others: [SubTypeEncryptedTest.new(secret: base64_secret, secret2: 'a secret')]) + expect_serialized_secret(obj.others.first) + end + + def expect_serialized_secret(obj) + expect(obj.send(:serialized_attributes)['encrypted$secret']).to eq({alg: 'CB_MOBILE_CUSTOM', + ciphertext: base64_secret}) + expect(obj.send(:serialized_attributes)).not_to have_key 'secret' + expect(obj.send(:serialized_attributes)['encrypted$secret2']).to eq({alg: 'CB_MOBILE_CUSTOM', + ciphertext: 'a secret'}) + expect(obj.send(:serialized_attributes)).not_to have_key 'secret2' + expect(JSON.parse(obj.to_json)['secret']).to eq base64_secret + expect(obj.as_json['secret']).to eq base64_secret + expect(obj.as_json['secret2']).to eq 'a secret' + end + + it 'prefix with custom algo' do + obj = SpecificAlgoTest.new(secret: base64_secret) + expect(obj.send(:serialized_attributes)['encrypted$secret']).to eq({alg: '3DES', ciphertext: base64_secret}) + expect(obj.send(:serialized_attributes)).not_to include 'secret' + end + + it 'decode encrypted attribute at reload' do + obj = TypeEncryptedTest.create!( + secret: base64_secret, + ) + obj.save! + obj.reload + expect(obj.secret).to eq base64_secret + end + + it 'decode nested encrypted attribute at reload' do + obj = TypeEncryptedTest.create!( + main: SubTypeEncryptedTest.new(secret: base64_secret), + ) + obj.save! + obj.reload + expect(obj.main.secret).to eq base64_secret + end + + it 'decode array encrypted attribute at reload' do + obj = TypeEncryptedTest.create!( + others: [SubTypeEncryptedTest.new(secret: base64_secret)] + ) + obj.save! + obj.reload + expect(obj.others.first.secret).to eq base64_secret + end + + it 'decode encrypted attribute at load' do + obj = TypeEncryptedTest.create!( + secret: base64_secret, + ) + obj.save! + obj = TypeEncryptedTest.find(obj.id) + expect(obj.secret).to eq base64_secret + end + + it 'decode nested encrypted attribute at load' do + obj = TypeEncryptedTest.create!( + main: SubTypeEncryptedTest.new(secret: base64_secret), + ) + obj.save! + obj = TypeEncryptedTest.find(obj.id) + + expect(obj.main.secret).to eq base64_secret + end + + it 'decode array encrypted attribute at load' do + obj = TypeEncryptedTest.create!( + others: [SubTypeEncryptedTest.new(secret: base64_secret)] + ) + obj.save! + obj = TypeEncryptedTest.find(obj.id) + + expect(obj.others.first.secret).to eq base64_secret + end end diff --git a/spec/type_nested_spec.rb b/spec/type_nested_spec.rb index 958fd56b..9eb831ce 100644 --- a/spec/type_nested_spec.rb +++ b/spec/type_nested_spec.rb @@ -1,191 +1,193 @@ -require File.expand_path("../support", __FILE__) +# frozen_string_literal: true -require "active_model" +require File.expand_path('support', __dir__) + +require 'active_model' class SubTypeTest < CouchbaseOrm::NestedDocument - attribute :name, :string - attribute :tags, :array, type: :string - attribute :milestones, :array, type: :date - attribute :flags, :array, type: :boolean - attribute :things - attribute :child, :nested, type: SubTypeTest + attribute :name, :string + attribute :tags, :array, type: :string + attribute :milestones, :array, type: :date + attribute :flags, :array, type: :boolean + attribute :things + attribute :child, :nested, type: SubTypeTest end class TypeNestedTest < CouchbaseOrm::Base - attribute :main, :nested, type: SubTypeTest - attribute :others, :array, type: SubTypeTest - attribute :flags, :array, type: :boolean + attribute :main, :nested, type: SubTypeTest + attribute :others, :array, type: SubTypeTest + attribute :flags, :array, type: :boolean end describe CouchbaseOrm::Types::Nested do - it "should be able to store and retrieve a nested object" do - obj = TypeNestedTest.new - obj.main = SubTypeTest.new - obj.main.name = "foo" - obj.main.tags = ["foo", "bar"] - obj.main.child = SubTypeTest.new(name: "bar") - obj.save! - - obj = TypeNestedTest.find(obj.id) - expect(obj.main.name).to eq "foo" - expect(obj.main.tags).to eq ["foo", "bar"] - expect(obj.main.child.name).to eq "bar" + it 'is able to store and retrieve a nested object' do + obj = TypeNestedTest.new + obj.main = SubTypeTest.new + obj.main.name = 'foo' + obj.main.tags = ['foo', 'bar'] + obj.main.child = SubTypeTest.new(name: 'bar') + obj.save! + + obj = TypeNestedTest.find(obj.id) + expect(obj.main.name).to eq 'foo' + expect(obj.main.tags).to eq ['foo', 'bar'] + expect(obj.main.child.name).to eq 'bar' + end + + it 'is able to store and retrieve an array of nested objects' do + obj = TypeNestedTest.new + obj.others = [SubTypeTest.new, SubTypeTest.new] + obj.others[0].name = 'foo' + obj.others[0].tags = ['foo', 'bar'] + obj.others[1].name = 'bar' + obj.others[1].tags = ['bar', 'baz'] + obj.others[1].child = SubTypeTest.new(name: 'baz') + obj.save! + + obj = TypeNestedTest.find(obj.id) + expect(obj.others[0].name).to eq 'foo' + expect(obj.others[0].tags).to eq ['foo', 'bar'] + expect(obj.others[1].name).to eq 'bar' + expect(obj.others[1].tags).to eq ['bar', 'baz'] + expect(obj.others[1].child.name).to eq 'baz' + end + + it 'serializes to JSON' do + obj = TypeNestedTest.new + obj.others = [SubTypeTest.new, SubTypeTest.new] + obj.others[0].name = 'foo' + obj.others[0].tags = ['foo', 'bar'] + obj.others[1].name = 'bar' + obj.others[1].tags = ['bar', 'baz'] + obj.others[1].child = SubTypeTest.new(name: 'baz') + obj.save! + + obj = TypeNestedTest.find(obj.id) + expect(obj.send(:serialized_attributes)).to eq({ + 'id' => obj.id, + 'main' => nil, + 'flags' => [], + 'others' => [ + { + 'name' => 'foo', + 'tags' => ['foo', 'bar'], + 'milestones' => [], + 'flags' => [], + 'things' => nil, + 'child' => nil + }, + { + 'name' => 'bar', + 'tags' => ['bar', 'baz'], + 'milestones' => [], + 'flags' => [], + 'things' => nil, + 'child' => { + 'name' => 'baz', + 'tags' => [], + 'milestones' => [], + 'flags' => [], + 'things' => nil, + 'child' => nil + } + } + ] + }) + end + + it 'does not have a save method' do + expect(SubTypeTest.new).not_to respond_to(:save) + end + + it 'does not cast a list' do + expect{ CouchbaseOrm::Types::Nested.new(type: SubTypeTest).cast([1, 2, 3]) }.to raise_error(ArgumentError) + end + + it 'does not serialize a list' do + expect{ CouchbaseOrm::Types::Nested.new(type: SubTypeTest).serialize([1, 2, 3]) }.to raise_error(ArgumentError) + end + + it 'saves a object with nested changes' do + obj = TypeNestedTest.new + obj.main = SubTypeTest.new(name: 'foo') + obj.others = [SubTypeTest.new(name: 'foo'), SubTypeTest.new(name: 'bar')] + obj.flags = [false, true] + obj.save! + obj.main.name = 'bar' + obj.others[0].name = 'bar' + obj.others[1].name = 'baz' + obj.flags[0] = true + + obj.save! + obj = TypeNestedTest.find(obj.id) + expect(obj.main.name).to eq 'bar' + expect(obj.others[0].name).to eq 'bar' + expect(obj.others[1].name).to eq 'baz' + expect(obj.flags).to eq [true, true] + end + + describe 'Validations' do + class SubWithValidation < CouchbaseOrm::NestedDocument + attribute :id, :string + attribute :name + attribute :label + attribute :child, :nested, type: SubWithValidation + validates :name, presence: true + validates :child, nested: true end - it "should be able to store and retrieve an array of nested objects" do - obj = TypeNestedTest.new - obj.others = [SubTypeTest.new, SubTypeTest.new] - obj.others[0].name = "foo" - obj.others[0].tags = ["foo", "bar"] - obj.others[1].name = "bar" - obj.others[1].tags = ["bar", "baz"] - obj.others[1].child = SubTypeTest.new(name: "baz") - obj.save! - - obj = TypeNestedTest.find(obj.id) - expect(obj.others[0].name).to eq "foo" - expect(obj.others[0].tags).to eq ["foo", "bar"] - expect(obj.others[1].name).to eq "bar" - expect(obj.others[1].tags).to eq ["bar", "baz"] - expect(obj.others[1].child.name).to eq "baz" + class WithValidationParent < CouchbaseOrm::Base + attribute :child, :nested, type: SubWithValidation + attribute :children, :array, type: SubWithValidation + validates :child, :children, nested: true end - it "should serialize to JSON" do - obj = TypeNestedTest.new - obj.others = [SubTypeTest.new, SubTypeTest.new] - obj.others[0].name = "foo" - obj.others[0].tags = ["foo", "bar"] - obj.others[1].name = "bar" - obj.others[1].tags = ["bar", "baz"] - obj.others[1].child = SubTypeTest.new(name: "baz") - obj.save! - - obj = TypeNestedTest.find(obj.id) - expect(obj.send(:serialized_attributes)).to eq ({ - "id" => obj.id, - "main" => nil, - "flags" => [], - "others" => [ - { - "name" => "foo", - "tags" => ["foo", "bar"], - "milestones" => [], - "flags" => [], - "things" => nil, - "child" => nil - }, - { - "name" => "bar", - "tags" => ["bar", "baz"], - "milestones" => [], - "flags" => [], - "things" => nil, - "child" => { - "name" => "baz", - "tags" => [], - "milestones" => [], - "flags" => [], - "things" => nil, - "child" => nil - } - } - ] - }) + it 'generates an id' do + expect(SubWithValidation.new.id).to be_present end - - it "should not have a save method" do - expect(SubTypeTest.new).to_not respond_to(:save) + + it 'does not regenerate the id after reloading parent' do + obj = WithValidationParent.new + obj.child = SubWithValidation.new(name: 'foo') + obj.save! + expect(obj.child.id).to be_present + old_id = obj.child.id + obj.reload + expect(obj.child.id).to eq(old_id) end - it "should not cast a list" do - expect{CouchbaseOrm::Types::Nested.new(type: SubTypeTest).cast([1,2,3])}.to raise_error(ArgumentError) + it 'does not override the param id' do + expect(SubWithValidation.new(id: 'foo').id).to eq 'foo' end - - it "should not serialize a list" do - expect{CouchbaseOrm::Types::Nested.new(type: SubTypeTest).serialize([1,2,3])}.to raise_error(ArgumentError) + + it 'validates the nested object' do + obj = WithValidationParent.new + obj.child = SubWithValidation.new + expect(obj).not_to be_valid + expect(obj.errors[:child]).to eq ['is invalid'] + expect(obj.child.errors[:name]).to eq ["can't be blank"] end - it "should save a object with nested changes" do - obj = TypeNestedTest.new - obj.main = SubTypeTest.new(name: "foo") - obj.others = [SubTypeTest.new(name: "foo"), SubTypeTest.new(name: "bar")] - obj.flags = [false, true] - obj.save! - obj.main.name = "bar" - obj.others[0].name = "bar" - obj.others[1].name = "baz" - obj.flags[0] = true - - obj.save! - obj = TypeNestedTest.find(obj.id) - expect(obj.main.name).to eq "bar" - expect(obj.others[0].name).to eq "bar" - expect(obj.others[1].name).to eq "baz" - expect(obj.flags).to eq [true, true] + it 'validates the nested objects in an array' do + obj = WithValidationParent.new + obj.children = [SubWithValidation.new(name: 'foo'), SubWithValidation.new] + expect(obj).not_to be_valid + expect(obj.errors[:children]).to eq ['is invalid'] + expect(obj.children[1].errors[:name]).to eq ["can't be blank"] end - describe "Validations" do - class SubWithValidation < CouchbaseOrm::NestedDocument - attribute :id, :string - attribute :name - attribute :label - attribute :child, :nested, type: SubWithValidation - validates :name, presence: true - validates :child, nested: true - end - - class WithValidationParent < CouchbaseOrm::Base - attribute :child, :nested, type: SubWithValidation - attribute :children, :array, type: SubWithValidation - validates :child, :children, nested: true - end - - it "should generate an id" do - expect(SubWithValidation.new.id).to be_present - end - - it "should not regenerate the id after reloading parent" do - obj = WithValidationParent.new - obj.child = SubWithValidation.new(name: "foo") - obj.save! - expect(obj.child.id).to be_present - old_id = obj.child.id - obj.reload - expect(obj.child.id).to eq(old_id) - end - - it "should not override the param id" do - expect(SubWithValidation.new(id: "foo").id).to eq "foo" - end - - it "should validate the nested object" do - obj = WithValidationParent.new - obj.child = SubWithValidation.new - expect(obj).to_not be_valid - expect(obj.errors[:child]).to eq ["is invalid"] - expect(obj.child.errors[:name]).to eq ["can't be blank"] - end - - it "should validate the nested objects in an array" do - obj = WithValidationParent.new - obj.children = [SubWithValidation.new(name: "foo"), SubWithValidation.new] - expect(obj).to_not be_valid - expect(obj.errors[:children]).to eq ["is invalid"] - expect(obj.children[1].errors[:name]).to eq ["can't be blank"] - end - - it "should validate the nested in the nested object" do - obj = WithValidationParent.new - obj.child = SubWithValidation.new name: "foo", label: "parent" - obj.child.child = SubWithValidation.new label: "child" - - expect(obj).to_not be_valid - expect(obj.child).to_not be_valid - expect(obj.child.child).to_not be_valid - - expect(obj.errors[:child]).to eq ["is invalid"] - expect(obj.child.errors[:child]).to eq ["is invalid"] - expect(obj.child.child.errors[:name]).to eq ["can't be blank"] - end + it 'validates the nested in the nested object' do + obj = WithValidationParent.new + obj.child = SubWithValidation.new name: 'foo', label: 'parent' + obj.child.child = SubWithValidation.new label: 'child' + + expect(obj).not_to be_valid + expect(obj.child).not_to be_valid + expect(obj.child.child).not_to be_valid + + expect(obj.errors[:child]).to eq ['is invalid'] + expect(obj.child.errors[:child]).to eq ['is invalid'] + expect(obj.child.child.errors[:name]).to eq ["can't be blank"] end + end end diff --git a/spec/type_spec.rb b/spec/type_spec.rb index 753507f1..c60dc50c 100644 --- a/spec/type_spec.rb +++ b/spec/type_spec.rb @@ -1,7 +1,9 @@ -require File.expand_path("../support", __FILE__) -require "timecop" -require "active_model" -require "couchbase-orm/types" +# frozen_string_literal: true + +require File.expand_path('support', __dir__) +require 'timecop' +require 'active_model' +require 'couchbase-orm/types' class DateTimeWith3Decimal < CouchbaseOrm::Types::DateTime def serialize(value) @@ -12,333 +14,337 @@ def serialize(value) 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 :precision3_time, :datetime3decimal - attribute :precision6_time, :datetime, precision: 6 - - attribute :created_at, :datetime, precision: 6 - attribute :updated_at, :datetime, precision: 6 - - attribute :active, :boolean - - index :age, presence: false - index :renewal_date, presence: false - index :some_time, presence: false - index :precision3_time, presence: false + attribute :name, :string + attribute :age, :integer + attribute :size, :float + attribute :renewal_date, :date + attribute :subscribed_at, :datetime + attribute :some_time, :timestamp + attribute :precision3_time, :datetime3decimal + attribute :precision6_time, :datetime, precision: 6 + + attribute :created_at, :datetime, precision: 6 + attribute :updated_at, :datetime, precision: 6 + + attribute :active, :boolean + + index :age, presence: false + index :renewal_date, presence: false + index :some_time, presence: false + index :precision3_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 :precision3_time, :datetime3decimal - attribute :active, :boolean - attribute :address, :hash - - 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 :precision3_time, validate: false - n1ql :by_both_dates, emit_key: [:renewal_date, :subscribed_at], presence: false + attribute :name, :string + attribute :age, :integer + attribute :size, :float + attribute :renewal_date, :date + attribute :subscribed_at, :datetime + attribute :some_time, :timestamp + attribute :precision3_time, :datetime3decimal + attribute :active, :boolean + attribute :address, :hash + + 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 :precision3_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 + it 'casts 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 'casts 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 'casts 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 'serializes 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 + it 'gets 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.delete_all - N1qlTypeTest.delete_all - end + before do + TypeTest.delete_all + N1qlTypeTest.delete_all + end - it "should be typed" do - expect(N1qlTypeTest.attribute_types["name"]).to be_a(ActiveModel::Type::String) - end + it 'is typed' do + expect(N1qlTypeTest.attribute_types['name']).to be_a(ActiveModel::Type::String) + end - it "should be createable" do - t = TypeTest.create! - expect(t).to be_a(TypeTest) - end + it 'is 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 'is 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! + it 'is 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 + 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 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 'is 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 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 'is 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 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 'is 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 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 'is 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 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 'is 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 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 'is 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 custom type" do - now = Time.now - t = TypeTest.create!(precision3_time: now) - _t2 = TypeTest.create!(precision3_time: now + 1) - expect(TypeTest.find_by_precision3_time(now)).to eq t - end + it 'is 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 and type cast" do - now = Time.now - now_s = now.utc.iso8601(3) - t = TypeTest.create!(precision3_time: now_s) - expect(TypeTest.find_by_precision3_time(now)).to eq t - expect(TypeTest.find_by_precision3_time(now_s)).to eq t - end + it 'is able to query by custom type' do + now = Time.now + t = TypeTest.create!(precision3_time: now) + _t2 = TypeTest.create!(precision3_time: now + 1) + expect(TypeTest.find_by_precision3_time(now)).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! + it 'is able to query by custom type and type cast' do + now = Time.now + now_s = now.utc.iso8601(3) + t = TypeTest.create!(precision3_time: now_s) + expect(TypeTest.find_by_precision3_time(now)).to eq t + expect(TypeTest.find_by_precision3_time(now_s)).to eq t + end - 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 'is 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! + it 'is 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 - 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 'is 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! - 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 - 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 'is 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 - 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 + it 'is 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 - it "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 - - it "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 'is 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 contain_exactly(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 'is 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 contain_exactly(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 'is 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 'is 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 'is 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 'is 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!(precision3_time: now) - _t2 = N1qlTypeTest.create!(precision3_time: now + 1) - expect(N1qlTypeTest.find_by_precision3_time(now).to_a).to eq [t] - end + it 'is able to query by custom type' do + now = Time.now + t = N1qlTypeTest.create!(precision3_time: now) + _t2 = N1qlTypeTest.create!(precision3_time: now + 1) + expect(N1qlTypeTest.find_by_precision3_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 'is 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 + it 'is 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 - it "should set datetime with precision" do - time = Time.at(1667499592.5170466123) - Timecop.freeze(time) do - test = TypeTest.create!(precision3_time: 1667499592.5170466123, some_time: 1667499592.5170466123, precision6_time: Time.now) + it 'sets datetime with precision' do + time = Time.at(1667499592.5170466123) + Timecop.freeze(time) do + test = TypeTest.create!(precision3_time: 1667499592.5170466123, some_time: 1667499592.5170466123, + precision6_time: Time.now) - expect(test.created_at).to eq(time.floor(6)) - expect(test.updated_at).to eq(time.floor(6)) + expect(test.created_at).to eq(time.floor(6)) + expect(test.updated_at).to eq(time.floor(6)) - expect(test.some_time).to eq(time.floor) - expect(test.precision3_time).to eq(time.floor(3)) - expect(test.precision6_time).to eq(time.floor(6)) - end + expect(test.some_time).to eq(time.floor) + expect(test.precision3_time).to eq(time.floor(3)) + expect(test.precision6_time).to eq(time.floor(6)) end + end end describe CouchbaseOrm::Types::Hash do - it 'should cast Hash to HashWithIndifferentAccess' do - expect(CouchbaseOrm::Types::Hash.new.cast({'a' => 1}).class).to be(HashWithIndifferentAccess) - end + it 'casts Hash to HashWithIndifferentAccess' do + expect(CouchbaseOrm::Types::Hash.new.cast({'a' => 1}).class).to be(HashWithIndifferentAccess) + end - it 'should cast nil to nil' do - expect(CouchbaseOrm::Types::Hash.new.cast(nil)).to be_nil - end + it 'casts nil to nil' do + expect(CouchbaseOrm::Types::Hash.new.cast(nil)).to be_nil + end - it 'should cast HashWithIndifferentAccess to HashWithIndifferentAccess' do - expect(CouchbaseOrm::Types::Hash.new.cast({'a' => 1}.with_indifferent_access).class).to be(HashWithIndifferentAccess) - end + it 'casts HashWithIndifferentAccess to HashWithIndifferentAccess' do + expect(CouchbaseOrm::Types::Hash.new.cast({'a' => 1}.with_indifferent_access).class).to be(HashWithIndifferentAccess) + end - it 'should serialize Hash as json hash' do - expect(CouchbaseOrm::Types::Hash.new.serialize({'a' => 1}).class).to be(Hash) - end + it 'serializes Hash as json hash' do + expect(CouchbaseOrm::Types::Hash.new.serialize({'a' => 1}).class).to be(Hash) + end - it 'should serialize HashWithIndifferentAccess as json hash' do - expect(CouchbaseOrm::Types::Hash.new.serialize({'a' => 1}.with_indifferent_access).class).to be(Hash) - end + it 'serializes HashWithIndifferentAccess as json hash' do + expect(CouchbaseOrm::Types::Hash.new.serialize({'a' => 1}.with_indifferent_access).class).to be(Hash) + end - it 'should serialize nil as nil' do - expect(CouchbaseOrm::Types::Hash.new.serialize(nil)).to be_nil - end + it 'serializes nil as nil' do + expect(CouchbaseOrm::Types::Hash.new.serialize(nil)).to be_nil + end end diff --git a/spec/views_spec.rb b/spec/views_spec.rb index 3ae3f865..0b675cc0 100644 --- a/spec/views_spec.rb +++ b/spec/views_spec.rb @@ -1,94 +1,92 @@ # frozen_string_literal: true, encoding: ASCII-8BIT +# frozen_string_literal: true -require File.expand_path("../support", __FILE__) +require File.expand_path('support', __dir__) require 'set' class ViewTest < CouchbaseOrm::Base - attribute :name, type: String - enum rating: [:awesome, :good, :okay, :bad], default: :okay + attribute :name, type: String + enum rating: [:awesome, :good, :okay, :bad], default: :okay - view :vall + view :vall # This generates both: # view :by_rating, emit_key: :rating # def self.find_by_rating(rating); end # also provide this helper function - index_view :rating + index_view :rating end - describe CouchbaseOrm::Views do - before(:each) do - ViewTest.delete_all - 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.delete_all + before do + ViewTest.delete_all + 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 do + ViewTest.delete_all + rescue Couchbase::Error::InternalServerFailure + # ignore (FIXME: check before merge) + rescue Couchbase::Error::DesignDocumentNotFound + # ignore (FIXME: check before merge) (7.1) + end + + it 'does not allow n1ql to override existing methods' do + expect { ViewTest.view :all }.to raise_error(ArgumentError) + end + + it 'saves a new design document' do + begin + ViewTest.bucket.view_indexes.drop_design_document(ViewTest.design_document, :production) rescue Couchbase::Error::InternalServerFailure - # ignore (FIXME: check before merge) + # ignore if design document does not exist rescue Couchbase::Error::DesignDocumentNotFound - # ignore (FIXME: check before merge) (7.1) - end - - it "should not allow n1ql to override existing methods" do - expect { ViewTest.view :all }.to raise_error(ArgumentError) - end - - it "should save a new design document" do - begin - 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 - - 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.vall).to eq([]) - end - - it "should perform a map-reduce and return the view" do - ViewTest.ensure_design_document! - ViewTest.create! name: :bob, rating: :good - - docs = ViewTest.vall.collect { |ob| - ob.destroy - ob.name - } - expect(docs).to eq(['bob']) - end - - it "should work with other keys" do - ViewTest.ensure_design_document! - ViewTest.create! name: :bob, rating: :good - ViewTest.create! name: :jane, rating: :awesome - ViewTest.create! name: :greg, rating: :bad - - docs = ViewTest.by_rating(order: :descending).collect { |ob| - ob.destroy - ob.name - } - expect(docs).to eq(['greg', 'bob', 'jane']) - end - - it "should return matching results" do - ViewTest.ensure_design_document! - ViewTest.create! name: :bob, rating: :awesome - ViewTest.create! name: :jane, rating: :awesome - ViewTest.create! name: :greg, rating: :bad - ViewTest.create! name: :mel, rating: :good - - docs = ViewTest.find_by_rating(1).collect { |ob| - ob.name - } - - expect(Set.new(docs)).to eq(Set.new(['bob', 'jane'])) + # ignore if design document does not exist (7.1) end + expect(ViewTest.ensure_design_document!).to be(true) + end + + it 'does not re-save a design doc if nothing has changed' do + expect(ViewTest.ensure_design_document!).to be(false) + end + + it 'returns an empty array when there is no objects' do + expect(ViewTest.vall).to eq([]) + end + + it 'performs a map-reduce and return the view' do + ViewTest.ensure_design_document! + ViewTest.create! name: :bob, rating: :good + + docs = ViewTest.vall.collect { |ob| + ob.destroy + ob.name + } + expect(docs).to eq(['bob']) + end + + it 'works with other keys' do + ViewTest.ensure_design_document! + ViewTest.create! name: :bob, rating: :good + ViewTest.create! name: :jane, rating: :awesome + ViewTest.create! name: :greg, rating: :bad + + docs = ViewTest.by_rating(order: :descending).collect { |ob| + ob.destroy + ob.name + } + expect(docs).to eq(['greg', 'bob', 'jane']) + end + + it 'returns matching results' do + ViewTest.ensure_design_document! + ViewTest.create! name: :bob, rating: :awesome + ViewTest.create! name: :jane, rating: :awesome + ViewTest.create! name: :greg, rating: :bad + ViewTest.create! name: :mel, rating: :good + + docs = ViewTest.find_by_rating(1).collect(&:name) + + expect(Set.new(docs)).to eq(Set.new(['bob', 'jane'])) + end end