mongoid_orderable 5.0.0 → 6.0.2
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 +83 -22
- data/LICENSE.txt +20 -0
- data/README.md +256 -149
- 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/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/handlers/base.rb +167 -0
- data/lib/mongoid/orderable/handlers/document.rb +24 -0
- data/lib/mongoid/orderable/handlers/document_embedded.rb +14 -0
- data/lib/mongoid/orderable/handlers/document_transactional.rb +35 -0
- data/lib/mongoid/orderable/handlers/transaction.rb +71 -0
- data/lib/mongoid/orderable/installer.rb +63 -0
- data/lib/mongoid/orderable/mixins/callbacks.rb +43 -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 +33 -54
- data/spec/integration/concurrency_spec.rb +232 -0
- data/spec/integration/customized_spec.rb +31 -0
- data/spec/integration/embedded_spec.rb +41 -0
- data/spec/integration/foreign_key_spec.rb +33 -0
- data/spec/integration/inherited_spec.rb +54 -0
- data/spec/integration/multiple_fields_spec.rb +554 -0
- data/spec/integration/multiple_scoped_spec.rb +63 -0
- data/spec/integration/no_indexed_spec.rb +23 -0
- data/spec/integration/scoped_spec.rb +151 -0
- data/spec/integration/simple_spec.rb +184 -0
- data/spec/integration/string_scoped_spec.rb +28 -0
- data/spec/integration/zero_based_spec.rb +161 -0
- data/spec/spec_helper.rb +42 -30
- data/spec/support/models.rb +122 -0
- metadata +75 -41
- data/.gitignore +0 -4
- data/.rspec +0 -2
- data/.rvmrc +0 -1
- data/.travis.yml +0 -14
- data/Gemfile +0 -18
- 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 -26
- data/spec/mongoid/orderable_spec.rb +0 -1413
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Generators
|
6
|
+
class Position < Base
|
7
|
+
def generate(field_name)
|
8
|
+
klass.class_eval <<~KLASS, __FILE__, __LINE__ + 1
|
9
|
+
def orderable_position(field = nil)
|
10
|
+
field ||= default_orderable_field
|
11
|
+
send "orderable_\#{field}_position"
|
12
|
+
end
|
13
|
+
KLASS
|
14
|
+
|
15
|
+
generate_method("orderable_#{field_name}_position") do
|
16
|
+
send field_name
|
17
|
+
end
|
18
|
+
|
19
|
+
generate_method("orderable_#{field_name}_position=") do |value|
|
20
|
+
send "#{field_name}=", value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Generators
|
6
|
+
class Scope < Base
|
7
|
+
def generate(field_name, order_scope)
|
8
|
+
criteria = criteria(order_scope)
|
9
|
+
klass.class_eval do
|
10
|
+
scope "orderable_#{field_name}_scope", criteria
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def criteria(order_scope)
|
17
|
+
case order_scope
|
18
|
+
when Proc then order_scope
|
19
|
+
when Array then ->(doc) { where(order_scope.each_with_object({}) {|f, h| h[f] = doc.send(f) }) }
|
20
|
+
else ->(_doc) { where({}) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Handlers
|
6
|
+
class Base
|
7
|
+
attr_reader :doc
|
8
|
+
|
9
|
+
def initialize(doc)
|
10
|
+
@doc = doc
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
delegate :orderable_keys,
|
16
|
+
:orderable_field,
|
17
|
+
:orderable_position,
|
18
|
+
:orderable_scope,
|
19
|
+
:orderable_scope_changed?,
|
20
|
+
:orderable_top,
|
21
|
+
:orderable_bottom,
|
22
|
+
:_id,
|
23
|
+
:new_record?,
|
24
|
+
:persisted?,
|
25
|
+
:embedded?,
|
26
|
+
:collection_name,
|
27
|
+
to: :doc
|
28
|
+
|
29
|
+
def use_transactions
|
30
|
+
false
|
31
|
+
end
|
32
|
+
|
33
|
+
def any_field_changed?
|
34
|
+
orderable_keys.any? {|field| changed?(field) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def apply_all_positions
|
38
|
+
orderable_keys.map {|field| apply_one_position(field, move_all[field]) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def apply_one_position(field, target_position)
|
42
|
+
return unless changed?(field)
|
43
|
+
|
44
|
+
set_lock(field) if use_transactions
|
45
|
+
|
46
|
+
f = orderable_field(field)
|
47
|
+
scope = orderable_scope(field)
|
48
|
+
scope_changed = orderable_scope_changed?(field)
|
49
|
+
|
50
|
+
# Set scope-level lock if scope changed
|
51
|
+
if use_transactions && persisted? && scope_changed
|
52
|
+
set_lock(field, true)
|
53
|
+
scope_changed = orderable_scope_changed?(field)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get the current position as exists in the database
|
57
|
+
current = if !persisted? || scope_changed
|
58
|
+
nil
|
59
|
+
elsif persisted? && !embedded?
|
60
|
+
scope.where(_id: _id).pluck(f).first
|
61
|
+
else
|
62
|
+
orderable_position(field)
|
63
|
+
end
|
64
|
+
|
65
|
+
# If scope changed, remove the position from the old scope
|
66
|
+
if persisted? && !embedded? && scope_changed
|
67
|
+
existing_doc = doc.class.unscoped.find(_id)
|
68
|
+
self.class.new(existing_doc).send(:remove_one_position, field)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return if there is no instruction to change the position
|
72
|
+
in_list = persisted? && current
|
73
|
+
return if in_list && !target_position
|
74
|
+
|
75
|
+
# Use $inc operator to shift the position of the other documents
|
76
|
+
target = resolve_target_position(field, target_position, in_list)
|
77
|
+
if !in_list
|
78
|
+
scope.gte(f => target).inc(f => 1)
|
79
|
+
elsif target < current
|
80
|
+
scope.where(f => { '$gte' => target, '$lt' => current }).inc(f => 1)
|
81
|
+
elsif target > current
|
82
|
+
scope.where(f => { '$gt' => current, '$lte' => target }).inc(f => -1)
|
83
|
+
end
|
84
|
+
|
85
|
+
# If persisted, update the field in the database atomically
|
86
|
+
doc.set({ f => target }.merge(changed_scope_hash(field))) if use_transactions && persisted?
|
87
|
+
doc.send("orderable_#{field}_position=", target)
|
88
|
+
end
|
89
|
+
|
90
|
+
def remove_all_positions
|
91
|
+
orderable_keys.each do |field|
|
92
|
+
remove_one_position(field)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def remove_one_position(field)
|
97
|
+
f = orderable_field(field)
|
98
|
+
current = orderable_position(field)
|
99
|
+
set_lock(field) if use_transactions
|
100
|
+
orderable_scope(field).gt(f => current).inc(f => -1)
|
101
|
+
end
|
102
|
+
|
103
|
+
def move_all
|
104
|
+
doc.send(:move_all)
|
105
|
+
end
|
106
|
+
|
107
|
+
def resolve_target_position(field, target_position, in_list)
|
108
|
+
target_position ||= 'bottom'
|
109
|
+
|
110
|
+
unless target_position.is_a? Numeric
|
111
|
+
target_position = case target_position.to_s
|
112
|
+
when 'top' then (min ||= orderable_top(field))
|
113
|
+
when 'bottom' then (max ||= orderable_bottom(field, in_list))
|
114
|
+
when 'higher' then orderable_position(field).pred
|
115
|
+
when 'lower' then orderable_position(field).next
|
116
|
+
when /\A\d+\Z/ then target_position.to_i
|
117
|
+
else raise Mongoid::Orderable::Errors::InvalidTargetPosition.new(target_position)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
if target_position <= (min ||= orderable_top(field))
|
122
|
+
target_position = min
|
123
|
+
elsif target_position > (max ||= orderable_bottom(field, in_list))
|
124
|
+
target_position = max
|
125
|
+
end
|
126
|
+
|
127
|
+
target_position
|
128
|
+
end
|
129
|
+
|
130
|
+
def changed?(field)
|
131
|
+
return true if new_record? || !doc.send(orderable_field(field)) || move_all[field]
|
132
|
+
changeable_keys(field).any? {|f| doc.send("#{f}_changed?") }
|
133
|
+
end
|
134
|
+
|
135
|
+
def changeable_keys(field)
|
136
|
+
[orderable_field(field)] | scope_keys(field)
|
137
|
+
end
|
138
|
+
|
139
|
+
def scope_keys(field)
|
140
|
+
orderable_scope(field).selector.keys.map do |f|
|
141
|
+
doc.fields[f]&.options&.[](:as) || f
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def changed_scope_hash(field)
|
146
|
+
scope_keys(field).each_with_object({}) do |f, hash|
|
147
|
+
hash[f] = doc.send(f) if doc.send("#{f}_changed?")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def set_lock(field, generic = false)
|
152
|
+
return unless use_transactions
|
153
|
+
model_name = doc.class.orderable_configs[field][:lock_collection].to_s.singularize.classify
|
154
|
+
model = Mongoid::Orderable::Models.const_get(model_name)
|
155
|
+
attrs = lock_scope(field, generic)
|
156
|
+
model.where(attrs).find_one_and_update(attrs, { upsert: true })
|
157
|
+
end
|
158
|
+
|
159
|
+
def lock_scope(field, generic = false)
|
160
|
+
sel = orderable_scope(field).selector
|
161
|
+
scope = ([collection_name] + (generic ? [field] : sel.to_a.flatten)).map(&:to_s).join('|')
|
162
|
+
{ scope: scope }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Handlers
|
6
|
+
class Document < Base
|
7
|
+
def before_create
|
8
|
+
apply_all_positions
|
9
|
+
end
|
10
|
+
|
11
|
+
def after_create; end
|
12
|
+
|
13
|
+
def before_update
|
14
|
+
return unless any_field_changed?
|
15
|
+
apply_all_positions
|
16
|
+
end
|
17
|
+
|
18
|
+
def after_destroy
|
19
|
+
remove_all_positions
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Handlers
|
6
|
+
class DocumentTransactional < Document
|
7
|
+
def before_create
|
8
|
+
clear_all_positions
|
9
|
+
end
|
10
|
+
|
11
|
+
def after_create
|
12
|
+
apply_all_positions
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def apply_all_positions
|
18
|
+
with_transaction { super }
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear_all_positions
|
22
|
+
orderable_keys.each {|field| doc.send("orderable_#{field}_position=", nil) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def use_transactions
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_transaction(&block)
|
30
|
+
Mongoid::Orderable::Handlers::Transaction.new(doc).with_transaction(&block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Handlers
|
6
|
+
# Executes a block within the context of a MongoDB transaction.
|
7
|
+
class Transaction
|
8
|
+
THREAD_KEY = :__mongoid_orderable_in_txn
|
9
|
+
RETRY_SLEEP = 0.001
|
10
|
+
|
11
|
+
attr_reader :doc
|
12
|
+
|
13
|
+
def initialize(doc)
|
14
|
+
@doc = doc
|
15
|
+
end
|
16
|
+
|
17
|
+
def with_transaction(&block)
|
18
|
+
Mongoid::QueryCache.uncached do
|
19
|
+
if Thread.current[THREAD_KEY]
|
20
|
+
yield
|
21
|
+
else
|
22
|
+
Thread.current[THREAD_KEY] = true
|
23
|
+
retries = transaction_max_retries
|
24
|
+
begin
|
25
|
+
do_transaction(&block)
|
26
|
+
rescue Mongo::Error::OperationFailure => e
|
27
|
+
sleep(RETRY_SLEEP)
|
28
|
+
retries -= 1
|
29
|
+
retry if retries >= 0
|
30
|
+
raise Mongoid::Orderable::Errors::TransactionFailed.new(e)
|
31
|
+
ensure
|
32
|
+
Thread.current[THREAD_KEY] = nil
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def do_transaction(&_block)
|
41
|
+
doc.class.with_session(session_opts) do |session|
|
42
|
+
doc.class.with(persistence_opts) do
|
43
|
+
session.start_transaction(transaction_opts)
|
44
|
+
yield
|
45
|
+
session.commit_transaction
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def session_opts
|
51
|
+
{ read: { mode: :primary },
|
52
|
+
causal_consistency: true }
|
53
|
+
end
|
54
|
+
|
55
|
+
def persistence_opts
|
56
|
+
{ read: { mode: :primary } }
|
57
|
+
end
|
58
|
+
|
59
|
+
def transaction_opts
|
60
|
+
{ read: { mode: :primary },
|
61
|
+
read_concern: { level: 'majority' },
|
62
|
+
write_concern: { w: 'majority' } }
|
63
|
+
end
|
64
|
+
|
65
|
+
def transaction_max_retries
|
66
|
+
doc.orderable_keys.map {|k| doc.class.orderable_configs.dig(k, :transaction_max_retries) }.compact.max
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
class Installer
|
6
|
+
attr_reader :klass, :config
|
7
|
+
|
8
|
+
def initialize(klass, options = {})
|
9
|
+
@klass = klass
|
10
|
+
@config = Mongoid::Orderable::Configs::FieldConfig.new(klass, options).options
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup
|
14
|
+
add_db_field
|
15
|
+
add_db_index if config[:index]
|
16
|
+
save_config
|
17
|
+
include_mixins
|
18
|
+
generate_all_helpers
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def field_name
|
24
|
+
config[:field_options][:as] || config[:field]
|
25
|
+
end
|
26
|
+
|
27
|
+
def order_scope
|
28
|
+
config[:scope]
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_db_field
|
32
|
+
klass.field(config[:field], { type: Integer }.reverse_merge(config[:field_options]))
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_db_index
|
36
|
+
spec = [[config[:field], 1]]
|
37
|
+
config[:scope].each {|field| spec.unshift([field, 1]) } if config[:scope].is_a?(Array)
|
38
|
+
klass.index(Hash[spec])
|
39
|
+
end
|
40
|
+
|
41
|
+
def save_config
|
42
|
+
klass.orderable_configs ||= {}
|
43
|
+
klass.orderable_configs = klass.orderable_configs.merge(field_name => config)
|
44
|
+
end
|
45
|
+
|
46
|
+
def include_mixins
|
47
|
+
klass.send :include, Mixins::Helpers
|
48
|
+
klass.send :include, Mixins::Callbacks
|
49
|
+
klass.send :include, Mixins::Movable
|
50
|
+
klass.send :include, Mixins::Listable
|
51
|
+
end
|
52
|
+
|
53
|
+
def generate_all_helpers
|
54
|
+
Generators::Scope.new(klass).generate(field_name, order_scope)
|
55
|
+
Generators::Position.new(klass).generate(field_name)
|
56
|
+
Generators::Movable.new(klass).generate(field_name)
|
57
|
+
Generators::Listable.new(klass).generate(field_name)
|
58
|
+
Generators::Helpers.new(klass).generate
|
59
|
+
Generators::LockCollection.new.generate(config[:lock_collection])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mongoid
|
4
|
+
module Orderable
|
5
|
+
module Mixins
|
6
|
+
module Callbacks
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
ORDERABLE_TRANSACTION_KEY = :__mongoid_orderable_in_txn
|
10
|
+
|
11
|
+
included do
|
12
|
+
before_create :orderable_before_create
|
13
|
+
after_create :orderable_after_create, prepend: true
|
14
|
+
before_update :orderable_before_update
|
15
|
+
after_destroy :orderable_after_destroy, prepend: true
|
16
|
+
|
17
|
+
delegate :before_create,
|
18
|
+
:after_create,
|
19
|
+
:before_update,
|
20
|
+
:after_destroy,
|
21
|
+
to: :orderable_handler,
|
22
|
+
prefix: :orderable
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def orderable_handler
|
27
|
+
@orderable_handler ||= self.class.orderable_handler_class.new(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.orderable_handler_class
|
31
|
+
if embedded?
|
32
|
+
Mongoid::Orderable::Handlers::DocumentEmbedded
|
33
|
+
elsif orderable_configs.values.any? {|c| c[:use_transactions] }
|
34
|
+
Mongoid::Orderable::Handlers::DocumentTransactional
|
35
|
+
else
|
36
|
+
Mongoid::Orderable::Handlers::Document
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|