mongoid_orderable 4.1.1 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
 - data/CHANGELOG.md +77 -17
 - data/LICENSE.txt +20 -0
 - data/README.md +256 -127
 - data/Rakefile +24 -6
 - data/lib/config/locales/en.yml +12 -9
 - data/lib/mongoid/orderable.rb +29 -22
 - data/lib/mongoid/orderable/configs/field_config.rb +79 -0
 - data/lib/mongoid/orderable/configs/global_config.rb +26 -0
 - data/lib/mongoid/orderable/engine.rb +206 -0
 - data/lib/mongoid/orderable/errors/invalid_target_position.rb +19 -18
 - data/lib/mongoid/orderable/errors/transaction_failed.rb +20 -0
 - data/lib/mongoid/orderable/generators/base.rb +21 -0
 - data/lib/mongoid/orderable/generators/helpers.rb +29 -0
 - data/lib/mongoid/orderable/generators/listable.rb +41 -0
 - data/lib/mongoid/orderable/generators/lock_collection.rb +37 -0
 - data/lib/mongoid/orderable/generators/movable.rb +62 -0
 - data/lib/mongoid/orderable/generators/position.rb +26 -0
 - data/lib/mongoid/orderable/generators/scope.rb +26 -0
 - data/lib/mongoid/orderable/installer.rb +63 -0
 - data/lib/mongoid/orderable/mixins/callbacks.rb +29 -0
 - data/lib/mongoid/orderable/mixins/helpers.rb +39 -0
 - data/lib/mongoid/orderable/mixins/listable.rb +49 -0
 - data/lib/mongoid/orderable/mixins/movable.rb +60 -0
 - data/lib/mongoid/orderable/version.rb +7 -0
 - data/lib/mongoid_orderable.rb +29 -56
 - data/spec/mongoid/orderable_spec.rb +1486 -1408
 - data/spec/spec_helper.rb +21 -37
 - metadata +62 -42
 - data/.gitignore +0 -4
 - data/.rspec +0 -1
 - data/.rvmrc +0 -1
 - data/.travis.yml +0 -20
 - data/Gemfile +0 -15
 - data/lib/mongoid/orderable/callbacks.rb +0 -75
 - data/lib/mongoid/orderable/configuration.rb +0 -60
 - data/lib/mongoid/orderable/errors.rb +0 -2
 - data/lib/mongoid/orderable/errors/mongoid_orderable_error.rb +0 -14
 - data/lib/mongoid/orderable/generator.rb +0 -34
 - data/lib/mongoid/orderable/generator/helpers.rb +0 -29
 - data/lib/mongoid/orderable/generator/listable.rb +0 -41
 - data/lib/mongoid/orderable/generator/movable.rb +0 -62
 - data/lib/mongoid/orderable/generator/position.rb +0 -26
 - data/lib/mongoid/orderable/generator/scope.rb +0 -17
 - data/lib/mongoid/orderable/helpers.rb +0 -50
 - data/lib/mongoid/orderable/listable.rb +0 -49
 - data/lib/mongoid/orderable/movable.rb +0 -58
 - data/lib/mongoid/orderable/orderable_class.rb +0 -51
 - data/lib/mongoid_orderable/mongoid/contexts/enumerable.rb +0 -15
 - data/lib/mongoid_orderable/mongoid/contexts/mongo.rb +0 -18
 - data/lib/mongoid_orderable/mongoid/contextual/memory.rb +0 -15
 - data/lib/mongoid_orderable/mongoid/criteria.rb +0 -4
 - data/lib/mongoid_orderable/version.rb +0 -3
 - data/mongoid_orderable.gemspec +0 -25
 
    
        data/Rakefile
    CHANGED
    
    | 
         @@ -1,6 +1,24 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
       3 
     | 
    
         
            -
             
     | 
| 
       4 
     | 
    
         
            -
             
     | 
| 
       5 
     | 
    
         
            -
             
     | 
| 
       6 
     | 
    
         
            -
             
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'bundler'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            begin
         
     | 
| 
      
 6 
     | 
    
         
            +
              Bundler.setup(:default, :development)
         
     | 
| 
      
 7 
     | 
    
         
            +
            rescue Bundler::BundlerError => e
         
     | 
| 
      
 8 
     | 
    
         
            +
              warn e.message
         
     | 
| 
      
 9 
     | 
    
         
            +
              warn 'Run `bundle install` to install missing gems'
         
     | 
| 
      
 10 
     | 
    
         
            +
              exit e.status_code
         
     | 
| 
      
 11 
     | 
    
         
            +
            end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            Bundler::GemHelper.install_tasks
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            require 'rspec/core'
         
     | 
| 
      
 16 
     | 
    
         
            +
            require 'rspec/core/rake_task'
         
     | 
| 
      
 17 
     | 
    
         
            +
            RSpec::Core::RakeTask.new(:spec) do |spec|
         
     | 
| 
      
 18 
     | 
    
         
            +
              spec.pattern = FileList['spec/**/*_spec.rb']
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            require 'rubocop/rake_task'
         
     | 
| 
      
 22 
     | 
    
         
            +
            RuboCop::RakeTask.new(:rubocop)
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            task default: %i[rubocop spec]
         
     | 
    
        data/lib/config/locales/en.yml
    CHANGED
    
    | 
         @@ -1,9 +1,12 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            en:
         
     | 
| 
       2 
     | 
    
         
            -
              mongoid:
         
     | 
| 
       3 
     | 
    
         
            -
                errors:
         
     | 
| 
       4 
     | 
    
         
            -
                  messages:
         
     | 
| 
       5 
     | 
    
         
            -
                    invalid_target_position:
         
     | 
| 
       6 
     | 
    
         
            -
                      message: "`%{value}` is not an acceptable value for target position."
         
     | 
| 
       7 
     | 
    
         
            -
                      summary: "Mongoid::Orderable accepts: a Numeric, a numeric string,
         
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
      
 1 
     | 
    
         
            +
            en:
         
     | 
| 
      
 2 
     | 
    
         
            +
              mongoid:
         
     | 
| 
      
 3 
     | 
    
         
            +
                errors:
         
     | 
| 
      
 4 
     | 
    
         
            +
                  messages:
         
     | 
| 
      
 5 
     | 
    
         
            +
                    invalid_target_position:
         
     | 
| 
      
 6 
     | 
    
         
            +
                      message: "`%{value}` is not an acceptable value for target position."
         
     | 
| 
      
 7 
     | 
    
         
            +
                      summary: "Mongoid::Orderable accepts: a Numeric, a numeric string, :top, :bottom, or nil."
         
     | 
| 
      
 8 
     | 
    
         
            +
                      resolution: "You must give an acceptable value for target position."
         
     | 
| 
      
 9 
     | 
    
         
            +
                    transaction_failed:
         
     | 
| 
      
 10 
     | 
    
         
            +
                      message: "Transaction failed due to a database-level conflict."
         
     | 
| 
      
 11 
     | 
    
         
            +
                      summary: ""
         
     | 
| 
      
 12 
     | 
    
         
            +
                      resolution: "Please retry the transaction."
         
     | 
    
        data/lib/mongoid/orderable.rb
    CHANGED
    
    | 
         @@ -1,22 +1,29 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
       3 
     | 
    
         
            -
             
     | 
| 
       4 
     | 
    
         
            -
               
     | 
| 
       5 
     | 
    
         
            -
                 
     | 
| 
       6 
     | 
    
         
            -
             
     | 
| 
       7 
     | 
    
         
            -
                 
     | 
| 
       8 
     | 
    
         
            -
             
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
                 
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
             
     | 
| 
       19 
     | 
    
         
            -
                 
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
             
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Mongoid
         
     | 
| 
      
 4 
     | 
    
         
            +
              module Orderable
         
     | 
| 
      
 5 
     | 
    
         
            +
                MUTEX = Mutex.new
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 8 
     | 
    
         
            +
                  def configure
         
     | 
| 
      
 9 
     | 
    
         
            +
                    yield(config) if block_given?
         
     | 
| 
      
 10 
     | 
    
         
            +
                  end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  def config
         
     | 
| 
      
 13 
     | 
    
         
            +
                    @config || MUTEX.synchronize { @config = ::Mongoid::Orderable::Configs::GlobalConfig.new }
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                extend ActiveSupport::Concern
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                included do
         
     | 
| 
      
 20 
     | 
    
         
            +
                  class_attribute :orderable_configs
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                class_methods do
         
     | 
| 
      
 24 
     | 
    
         
            +
                  def orderable(options = {})
         
     | 
| 
      
 25 
     | 
    
         
            +
                    Mongoid::Orderable::Installer.new(self, options).setup
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
              end
         
     | 
| 
      
 29 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,79 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Mongoid
         
     | 
| 
      
 4 
     | 
    
         
            +
            module Orderable
         
     | 
| 
      
 5 
     | 
    
         
            +
            module Configs
         
     | 
| 
      
 6 
     | 
    
         
            +
              class FieldConfig
         
     | 
| 
      
 7 
     | 
    
         
            +
                CONFIG_OPTIONS = %i[field
         
     | 
| 
      
 8 
     | 
    
         
            +
                                    scope
         
     | 
| 
      
 9 
     | 
    
         
            +
                                    foreign_key
         
     | 
| 
      
 10 
     | 
    
         
            +
                                    inherited
         
     | 
| 
      
 11 
     | 
    
         
            +
                                    base
         
     | 
| 
      
 12 
     | 
    
         
            +
                                    index
         
     | 
| 
      
 13 
     | 
    
         
            +
                                    default
         
     | 
| 
      
 14 
     | 
    
         
            +
                                    use_transactions
         
     | 
| 
      
 15 
     | 
    
         
            +
                                    transaction_max_retries
         
     | 
| 
      
 16 
     | 
    
         
            +
                                    lock_collection].freeze
         
     | 
| 
      
 17 
     | 
    
         
            +
                ALIASES = { column: :field }.freeze
         
     | 
| 
      
 18 
     | 
    
         
            +
                FIELD_OPTIONS = %i[as].freeze
         
     | 
| 
      
 19 
     | 
    
         
            +
                VALID_OPTIONS = (CONFIG_OPTIONS | FIELD_OPTIONS).freeze
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                attr_reader :orderable_class,
         
     | 
| 
      
 22 
     | 
    
         
            +
                            :options
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                def initialize(parent, options = {})
         
     | 
| 
      
 25 
     | 
    
         
            +
                  @orderable_class = parent
         
     | 
| 
      
 26 
     | 
    
         
            +
                  assign_options(options)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  set_field_options
         
     | 
| 
      
 28 
     | 
    
         
            +
                  set_orderable_scope
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                def global_config
         
     | 
| 
      
 32 
     | 
    
         
            +
                  cfg = Mongoid::Orderable.config
         
     | 
| 
      
 33 
     | 
    
         
            +
                  { field: cfg.field,
         
     | 
| 
      
 34 
     | 
    
         
            +
                    as: cfg.as,
         
     | 
| 
      
 35 
     | 
    
         
            +
                    index: cfg.index,
         
     | 
| 
      
 36 
     | 
    
         
            +
                    base: cfg.base,
         
     | 
| 
      
 37 
     | 
    
         
            +
                    use_transactions: cfg.use_transactions,
         
     | 
| 
      
 38 
     | 
    
         
            +
                    transaction_max_retries: cfg.transaction_max_retries,
         
     | 
| 
      
 39 
     | 
    
         
            +
                    lock_collection: cfg.lock_collection }
         
     | 
| 
      
 40 
     | 
    
         
            +
                end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                protected
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                def assign_options(options)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  @options = global_config
         
     | 
| 
      
 46 
     | 
    
         
            +
                  return unless options.is_a?(Hash)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  @options = @options.merge(options.symbolize_keys.transform_keys {|k| ALIASES[k] || k }).slice(*VALID_OPTIONS)
         
     | 
| 
      
 48 
     | 
    
         
            +
                end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                def set_field_options
         
     | 
| 
      
 51 
     | 
    
         
            +
                  @options[:field_options] = {}
         
     | 
| 
      
 52 
     | 
    
         
            +
                  FIELD_OPTIONS.each do |key|
         
     | 
| 
      
 53 
     | 
    
         
            +
                    next unless @options.key?(key)
         
     | 
| 
      
 54 
     | 
    
         
            +
                    @options[:field_options][key] = @options.delete(key)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                def set_orderable_scope
         
     | 
| 
      
 59 
     | 
    
         
            +
                  return unless @options[:scope].class.in?([Array, Symbol, String])
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                  scope = Array(@options[:scope])
         
     | 
| 
      
 62 
     | 
    
         
            +
                  scope.map! do |value|
         
     | 
| 
      
 63 
     | 
    
         
            +
                    case value
         
     | 
| 
      
 64 
     | 
    
         
            +
                    when Symbol
         
     | 
| 
      
 65 
     | 
    
         
            +
                      relation = @orderable_class.relations[@options[:scope].to_s]&.key&.to_sym
         
     | 
| 
      
 66 
     | 
    
         
            +
                      relation || value
         
     | 
| 
      
 67 
     | 
    
         
            +
                    when String
         
     | 
| 
      
 68 
     | 
    
         
            +
                      value.to_sym
         
     | 
| 
      
 69 
     | 
    
         
            +
                    else
         
     | 
| 
      
 70 
     | 
    
         
            +
                      raise ArgumentError.new("Orderable :scope invalid: #{@options[:scope]}")
         
     | 
| 
      
 71 
     | 
    
         
            +
                    end
         
     | 
| 
      
 72 
     | 
    
         
            +
                  end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                  @options[:scope] = scope
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
              end
         
     | 
| 
      
 77 
     | 
    
         
            +
            end
         
     | 
| 
      
 78 
     | 
    
         
            +
            end
         
     | 
| 
      
 79 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,26 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Mongoid
         
     | 
| 
      
 4 
     | 
    
         
            +
            module Orderable
         
     | 
| 
      
 5 
     | 
    
         
            +
            module Configs
         
     | 
| 
      
 6 
     | 
    
         
            +
              class GlobalConfig
         
     | 
| 
      
 7 
     | 
    
         
            +
                attr_accessor :field,
         
     | 
| 
      
 8 
     | 
    
         
            +
                              :as,
         
     | 
| 
      
 9 
     | 
    
         
            +
                              :base,
         
     | 
| 
      
 10 
     | 
    
         
            +
                              :index,
         
     | 
| 
      
 11 
     | 
    
         
            +
                              :use_transactions,
         
     | 
| 
      
 12 
     | 
    
         
            +
                              :transaction_max_retries,
         
     | 
| 
      
 13 
     | 
    
         
            +
                              :lock_collection
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def initialize
         
     | 
| 
      
 16 
     | 
    
         
            +
                  self.field = :position
         
     | 
| 
      
 17 
     | 
    
         
            +
                  self.index = true
         
     | 
| 
      
 18 
     | 
    
         
            +
                  self.base = 1
         
     | 
| 
      
 19 
     | 
    
         
            +
                  self.use_transactions = false
         
     | 
| 
      
 20 
     | 
    
         
            +
                  self.transaction_max_retries = 10
         
     | 
| 
      
 21 
     | 
    
         
            +
                  self.lock_collection = :mongoid_orderable_locks
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
            end
         
     | 
| 
      
 25 
     | 
    
         
            +
            end
         
     | 
| 
      
 26 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,206 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Mongoid
         
     | 
| 
      
 4 
     | 
    
         
            +
            module Orderable
         
     | 
| 
      
 5 
     | 
    
         
            +
              class Engine
         
     | 
| 
      
 6 
     | 
    
         
            +
                ORDERABLE_TRANSACTION_KEY = :__mongoid_orderable_in_txn
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                attr_accessor :doc
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                def initialize(doc)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @doc = doc
         
     | 
| 
      
 12 
     | 
    
         
            +
                end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                # For new records, or if the orderable scope changes,
         
     | 
| 
      
 15 
     | 
    
         
            +
                # we must yield the save action inside the transaction.
         
     | 
| 
      
 16 
     | 
    
         
            +
                def update_positions(&_block)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  yield and return unless orderable_keys.any? {|field| changed?(field) }
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  new_record = new_record?
         
     | 
| 
      
 20 
     | 
    
         
            +
                  with_transaction do
         
     | 
| 
      
 21 
     | 
    
         
            +
                    orderable_keys.map {|field| apply_one_position(field, move_all[field]) }
         
     | 
| 
      
 22 
     | 
    
         
            +
                    yield if new_record
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  yield unless new_record
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                def remove_positions
         
     | 
| 
      
 29 
     | 
    
         
            +
                  orderable_keys.each do |field|
         
     | 
| 
      
 30 
     | 
    
         
            +
                    remove_one_position(field)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                def apply_one_position(field, target_position)
         
     | 
| 
      
 35 
     | 
    
         
            +
                  return unless changed?(field)
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  set_lock(field) if use_transactions && !embedded?
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                  f = orderable_field(field)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  scope = orderable_scope(field)
         
     | 
| 
      
 41 
     | 
    
         
            +
                  scope_changed = orderable_scope_changed?(field)
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  # Set scope-level lock if scope changed
         
     | 
| 
      
 44 
     | 
    
         
            +
                  if use_transactions && persisted? && !embedded? && scope_changed
         
     | 
| 
      
 45 
     | 
    
         
            +
                    set_lock(field, true)
         
     | 
| 
      
 46 
     | 
    
         
            +
                    scope_changed = orderable_scope_changed?(field)
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                  # Get the current position as exists in the database
         
     | 
| 
      
 50 
     | 
    
         
            +
                  current = if !persisted? || scope_changed
         
     | 
| 
      
 51 
     | 
    
         
            +
                              nil
         
     | 
| 
      
 52 
     | 
    
         
            +
                            elsif persisted? && !embedded?
         
     | 
| 
      
 53 
     | 
    
         
            +
                              scope.where(_id: _id).pluck(f).first
         
     | 
| 
      
 54 
     | 
    
         
            +
                            else
         
     | 
| 
      
 55 
     | 
    
         
            +
                              orderable_position(field)
         
     | 
| 
      
 56 
     | 
    
         
            +
                            end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                  # If scope changed, remove the position from the old scope
         
     | 
| 
      
 59 
     | 
    
         
            +
                  if persisted? && !embedded? && scope_changed
         
     | 
| 
      
 60 
     | 
    
         
            +
                    existing_doc = doc.class.unscoped.find(_id)
         
     | 
| 
      
 61 
     | 
    
         
            +
                    self.class.new(existing_doc).remove_one_position(field)
         
     | 
| 
      
 62 
     | 
    
         
            +
                  end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                  # Return if there is no instruction to change the position
         
     | 
| 
      
 65 
     | 
    
         
            +
                  in_list = persisted? && current
         
     | 
| 
      
 66 
     | 
    
         
            +
                  return if in_list && !target_position
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                  # Use $inc operator to shift the position of the other documents
         
     | 
| 
      
 69 
     | 
    
         
            +
                  target = resolve_target_position(field, target_position, in_list)
         
     | 
| 
      
 70 
     | 
    
         
            +
                  if !in_list
         
     | 
| 
      
 71 
     | 
    
         
            +
                    scope.gte(f => target).inc(f => 1)
         
     | 
| 
      
 72 
     | 
    
         
            +
                  elsif target < current
         
     | 
| 
      
 73 
     | 
    
         
            +
                    scope.where(f => { '$gte' => target, '$lt' => current }).inc(f => 1)
         
     | 
| 
      
 74 
     | 
    
         
            +
                  elsif target > current
         
     | 
| 
      
 75 
     | 
    
         
            +
                    scope.where(f => { '$gt' => current, '$lte' => target }).inc(f => -1)
         
     | 
| 
      
 76 
     | 
    
         
            +
                  end
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                  # If persisted, update the field in the database atomically
         
     | 
| 
      
 79 
     | 
    
         
            +
                  doc.set({ f => target }.merge(changed_scope_hash(field))) if use_transactions && persisted? && !embedded?
         
     | 
| 
      
 80 
     | 
    
         
            +
                  doc.send("orderable_#{field}_position=", target)
         
     | 
| 
      
 81 
     | 
    
         
            +
                end
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                def remove_one_position(field)
         
     | 
| 
      
 84 
     | 
    
         
            +
                  f = orderable_field(field)
         
     | 
| 
      
 85 
     | 
    
         
            +
                  current = orderable_position(field)
         
     | 
| 
      
 86 
     | 
    
         
            +
                  set_lock(field) if use_transactions && !embedded?
         
     | 
| 
      
 87 
     | 
    
         
            +
                  orderable_scope(field).gt(f => current).inc(f => -1)
         
     | 
| 
      
 88 
     | 
    
         
            +
                end
         
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
                protected
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                delegate :orderable_keys,
         
     | 
| 
      
 93 
     | 
    
         
            +
                         :orderable_field,
         
     | 
| 
      
 94 
     | 
    
         
            +
                         :orderable_position,
         
     | 
| 
      
 95 
     | 
    
         
            +
                         :orderable_scope,
         
     | 
| 
      
 96 
     | 
    
         
            +
                         :orderable_scope_changed?,
         
     | 
| 
      
 97 
     | 
    
         
            +
                         :orderable_top,
         
     | 
| 
      
 98 
     | 
    
         
            +
                         :orderable_bottom,
         
     | 
| 
      
 99 
     | 
    
         
            +
                         :_id,
         
     | 
| 
      
 100 
     | 
    
         
            +
                         :new_record?,
         
     | 
| 
      
 101 
     | 
    
         
            +
                         :persisted?,
         
     | 
| 
      
 102 
     | 
    
         
            +
                         :embedded?,
         
     | 
| 
      
 103 
     | 
    
         
            +
                         :collection_name,
         
     | 
| 
      
 104 
     | 
    
         
            +
                         to: :doc
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                def move_all
         
     | 
| 
      
 107 
     | 
    
         
            +
                  doc.send(:move_all)
         
     | 
| 
      
 108 
     | 
    
         
            +
                end
         
     | 
| 
      
 109 
     | 
    
         
            +
             
     | 
| 
      
 110 
     | 
    
         
            +
                def resolve_target_position(field, target_position, in_list)
         
     | 
| 
      
 111 
     | 
    
         
            +
                  target_position ||= 'bottom'
         
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
                  unless target_position.is_a? Numeric
         
     | 
| 
      
 114 
     | 
    
         
            +
                    target_position = case target_position.to_s
         
     | 
| 
      
 115 
     | 
    
         
            +
                                      when 'top' then (min ||= orderable_top(field))
         
     | 
| 
      
 116 
     | 
    
         
            +
                                      when 'bottom' then (max ||= orderable_bottom(field, in_list))
         
     | 
| 
      
 117 
     | 
    
         
            +
                                      when 'higher' then orderable_position(field).pred
         
     | 
| 
      
 118 
     | 
    
         
            +
                                      when 'lower' then orderable_position(field).next
         
     | 
| 
      
 119 
     | 
    
         
            +
                                      when /\A\d+\Z/ then target_position.to_i
         
     | 
| 
      
 120 
     | 
    
         
            +
                                      else raise Mongoid::Orderable::Errors::InvalidTargetPosition.new(target_position)
         
     | 
| 
      
 121 
     | 
    
         
            +
                                      end
         
     | 
| 
      
 122 
     | 
    
         
            +
                  end
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
                  if target_position <= (min ||= orderable_top(field))
         
     | 
| 
      
 125 
     | 
    
         
            +
                    target_position = min
         
     | 
| 
      
 126 
     | 
    
         
            +
                  elsif target_position > (max ||= orderable_bottom(field, in_list))
         
     | 
| 
      
 127 
     | 
    
         
            +
                    target_position = max
         
     | 
| 
      
 128 
     | 
    
         
            +
                  end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
                  target_position
         
     | 
| 
      
 131 
     | 
    
         
            +
                end
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                def changed?(field)
         
     | 
| 
      
 134 
     | 
    
         
            +
                  return true if new_record? || !doc.send(orderable_field(field)) || move_all[field]
         
     | 
| 
      
 135 
     | 
    
         
            +
                  changeable_keys(field).any? {|f| doc.send("#{f}_changed?") }
         
     | 
| 
      
 136 
     | 
    
         
            +
                end
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
                def changeable_keys(field)
         
     | 
| 
      
 139 
     | 
    
         
            +
                  [orderable_field(field)] | scope_keys(field)
         
     | 
| 
      
 140 
     | 
    
         
            +
                end
         
     | 
| 
      
 141 
     | 
    
         
            +
             
     | 
| 
      
 142 
     | 
    
         
            +
                def scope_keys(field)
         
     | 
| 
      
 143 
     | 
    
         
            +
                  orderable_scope(field).selector.keys.map do |f|
         
     | 
| 
      
 144 
     | 
    
         
            +
                    doc.fields[f]&.options&.[](:as) || f
         
     | 
| 
      
 145 
     | 
    
         
            +
                  end
         
     | 
| 
      
 146 
     | 
    
         
            +
                end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
                def changed_scope_hash(field)
         
     | 
| 
      
 149 
     | 
    
         
            +
                  scope_keys(field).each_with_object({}) do |f, hash|
         
     | 
| 
      
 150 
     | 
    
         
            +
                    hash[f] = doc.send(f) if doc.send("#{f}_changed?")
         
     | 
| 
      
 151 
     | 
    
         
            +
                  end
         
     | 
| 
      
 152 
     | 
    
         
            +
                end
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
                def set_lock(field, scope_changed = false)
         
     | 
| 
      
 155 
     | 
    
         
            +
                  return unless use_transactions && !embedded?
         
     | 
| 
      
 156 
     | 
    
         
            +
                  model_name = doc.class.orderable_configs[field][:lock_collection].to_s.singularize.classify
         
     | 
| 
      
 157 
     | 
    
         
            +
                  model = Mongoid::Orderable::Models.const_get(model_name)
         
     | 
| 
      
 158 
     | 
    
         
            +
                  attrs = lock_scope(field, scope_changed)
         
     | 
| 
      
 159 
     | 
    
         
            +
                  model.where(attrs).find_one_and_update(attrs, { upsert: true })
         
     | 
| 
      
 160 
     | 
    
         
            +
                end
         
     | 
| 
      
 161 
     | 
    
         
            +
             
     | 
| 
      
 162 
     | 
    
         
            +
                def lock_scope(field, scope_changed = false)
         
     | 
| 
      
 163 
     | 
    
         
            +
                  sel = orderable_scope(field).selector
         
     | 
| 
      
 164 
     | 
    
         
            +
                  scope = ([collection_name] + (scope_changed ? sel.keys : sel.to_a.flatten)).map(&:to_s).join('|')
         
     | 
| 
      
 165 
     | 
    
         
            +
                  { scope: scope }
         
     | 
| 
      
 166 
     | 
    
         
            +
                end
         
     | 
| 
      
 167 
     | 
    
         
            +
             
     | 
| 
      
 168 
     | 
    
         
            +
                def use_transactions
         
     | 
| 
      
 169 
     | 
    
         
            +
                  orderable_keys.any? {|k| doc.class.orderable_configs[k][:use_transactions] }
         
     | 
| 
      
 170 
     | 
    
         
            +
                end
         
     | 
| 
      
 171 
     | 
    
         
            +
             
     | 
| 
      
 172 
     | 
    
         
            +
                def transaction_max_retries
         
     | 
| 
      
 173 
     | 
    
         
            +
                  orderable_keys.map {|k| doc.class.orderable_configs[k][:transaction_max_retries] }.compact.max
         
     | 
| 
      
 174 
     | 
    
         
            +
                end
         
     | 
| 
      
 175 
     | 
    
         
            +
             
     | 
| 
      
 176 
     | 
    
         
            +
                def with_transaction(&_block)
         
     | 
| 
      
 177 
     | 
    
         
            +
                  Mongoid::QueryCache.uncached do
         
     | 
| 
      
 178 
     | 
    
         
            +
                    if use_transactions && !embedded? && !Thread.current[ORDERABLE_TRANSACTION_KEY]
         
     | 
| 
      
 179 
     | 
    
         
            +
                      Thread.current[ORDERABLE_TRANSACTION_KEY] = true
         
     | 
| 
      
 180 
     | 
    
         
            +
                      retries = transaction_max_retries
         
     | 
| 
      
 181 
     | 
    
         
            +
                      begin
         
     | 
| 
      
 182 
     | 
    
         
            +
                        doc.class.with_session(causal_consistency: true) do |session|
         
     | 
| 
      
 183 
     | 
    
         
            +
                          doc.class.with(read: { mode: :primary }) do
         
     | 
| 
      
 184 
     | 
    
         
            +
                            session.start_transaction(read: { mode: :primary },
         
     | 
| 
      
 185 
     | 
    
         
            +
                                                      read_concern: { level: 'majority' },
         
     | 
| 
      
 186 
     | 
    
         
            +
                                                      write_concern: { w: 'majority' })
         
     | 
| 
      
 187 
     | 
    
         
            +
                            yield
         
     | 
| 
      
 188 
     | 
    
         
            +
                            session.commit_transaction
         
     | 
| 
      
 189 
     | 
    
         
            +
                          end
         
     | 
| 
      
 190 
     | 
    
         
            +
                        end
         
     | 
| 
      
 191 
     | 
    
         
            +
                      rescue Mongo::Error::OperationFailure => e
         
     | 
| 
      
 192 
     | 
    
         
            +
                        sleep(0.001)
         
     | 
| 
      
 193 
     | 
    
         
            +
                        retries -= 1
         
     | 
| 
      
 194 
     | 
    
         
            +
                        retry if retries >= 0
         
     | 
| 
      
 195 
     | 
    
         
            +
                        raise Mongoid::Orderable::Errors::TransactionFailed.new(e)
         
     | 
| 
      
 196 
     | 
    
         
            +
                      ensure
         
     | 
| 
      
 197 
     | 
    
         
            +
                        Thread.current[ORDERABLE_TRANSACTION_KEY] = nil
         
     | 
| 
      
 198 
     | 
    
         
            +
                      end
         
     | 
| 
      
 199 
     | 
    
         
            +
                    else
         
     | 
| 
      
 200 
     | 
    
         
            +
                      yield
         
     | 
| 
      
 201 
     | 
    
         
            +
                    end
         
     | 
| 
      
 202 
     | 
    
         
            +
                  end
         
     | 
| 
      
 203 
     | 
    
         
            +
                end
         
     | 
| 
      
 204 
     | 
    
         
            +
              end
         
     | 
| 
      
 205 
     | 
    
         
            +
            end
         
     | 
| 
      
 206 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -1,18 +1,19 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
       2 
     | 
    
         
            -
             
     | 
| 
       3 
     | 
    
         
            -
             
     | 
| 
       4 
     | 
    
         
            -
             
     | 
| 
       5 
     | 
    
         
            -
             
     | 
| 
       6 
     | 
    
         
            -
             
     | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
       8 
     | 
    
         
            -
                   
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
             
     | 
| 
       11 
     | 
    
         
            -
             
     | 
| 
       12 
     | 
    
         
            -
             
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
             
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
            end
         
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Mongoid
         
     | 
| 
      
 4 
     | 
    
         
            +
            module Orderable
         
     | 
| 
      
 5 
     | 
    
         
            +
            module Errors
         
     | 
| 
      
 6 
     | 
    
         
            +
              class InvalidTargetPosition < ::Mongoid::Errors::MongoidError
         
     | 
| 
      
 7 
     | 
    
         
            +
                def initialize(value)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  super _compose_message(value)
         
     | 
| 
      
 9 
     | 
    
         
            +
                end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                private
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def _compose_message(value)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  compose_message 'invalid_target_position', value: value.inspect
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
            end
         
     | 
| 
      
 18 
     | 
    
         
            +
            end
         
     | 
| 
      
 19 
     | 
    
         
            +
            end
         
     |