mongoid_orderable 6.0.4 → 6.0.6

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +101 -93
  3. data/LICENSE.txt +20 -20
  4. data/README.md +280 -275
  5. data/Rakefile +24 -24
  6. data/lib/config/locales/en.yml +12 -12
  7. data/lib/mongoid/orderable/configs/field_config.rb +81 -81
  8. data/lib/mongoid/orderable/configs/global_config.rb +26 -26
  9. data/lib/mongoid/orderable/errors/invalid_target_position.rb +19 -19
  10. data/lib/mongoid/orderable/errors/transaction_failed.rb +20 -20
  11. data/lib/mongoid/orderable/generators/base.rb +21 -21
  12. data/lib/mongoid/orderable/generators/helpers.rb +37 -37
  13. data/lib/mongoid/orderable/generators/listable.rb +41 -41
  14. data/lib/mongoid/orderable/generators/lock_collection.rb +39 -39
  15. data/lib/mongoid/orderable/generators/movable.rb +62 -62
  16. data/lib/mongoid/orderable/generators/position.rb +26 -26
  17. data/lib/mongoid/orderable/generators/scope.rb +26 -26
  18. data/lib/mongoid/orderable/handlers/base.rb +215 -189
  19. data/lib/mongoid/orderable/handlers/document.rb +32 -24
  20. data/lib/mongoid/orderable/handlers/document_embedded.rb +14 -14
  21. data/lib/mongoid/orderable/handlers/document_transactional.rb +32 -35
  22. data/lib/mongoid/orderable/handlers/transaction.rb +75 -71
  23. data/lib/mongoid/orderable/installer.rb +64 -64
  24. data/lib/mongoid/orderable/mixins/callbacks.rb +45 -43
  25. data/lib/mongoid/orderable/mixins/cascadeable.rb +15 -15
  26. data/lib/mongoid/orderable/mixins/helpers.rb +39 -39
  27. data/lib/mongoid/orderable/mixins/listable.rb +49 -49
  28. data/lib/mongoid/orderable/mixins/movable.rb +64 -60
  29. data/lib/mongoid/orderable/version.rb +7 -7
  30. data/lib/mongoid/orderable.rb +29 -29
  31. data/lib/mongoid_orderable.rb +34 -34
  32. metadata +6 -93
  33. data/spec/integration/cascadeable_spec.rb +0 -12
  34. data/spec/integration/concurrency_spec.rb +0 -232
  35. data/spec/integration/conditional_spec.rb +0 -36
  36. data/spec/integration/customized_spec.rb +0 -31
  37. data/spec/integration/embedded_spec.rb +0 -63
  38. data/spec/integration/foreign_key_spec.rb +0 -33
  39. data/spec/integration/inherited_spec.rb +0 -54
  40. data/spec/integration/multiple_fields_spec.rb +0 -554
  41. data/spec/integration/multiple_scoped_spec.rb +0 -63
  42. data/spec/integration/no_indexed_spec.rb +0 -23
  43. data/spec/integration/scoped_spec.rb +0 -151
  44. data/spec/integration/simple_spec.rb +0 -184
  45. data/spec/integration/string_scoped_spec.rb +0 -28
  46. data/spec/integration/zero_based_spec.rb +0 -161
  47. data/spec/spec_helper.rb +0 -42
  48. data/spec/support/models.rb +0 -134
@@ -1,62 +1,62 @@
1
- # frozen_string_literal: true
2
-
3
- module Mongoid
4
- module Orderable
5
- module Generators
6
- class Movable < Base
7
- def generate(field_name)
8
- generate_move_to_helpers(field_name)
9
- generate_insert_at_helpers(field_name)
10
- generate_shorthand_helpers(field_name)
11
- end
12
-
13
- protected
14
-
15
- def generate_move_to_helpers(field_name)
16
- generate_method("move_#{field_name}_to") do |target_position|
17
- move_field_to target_position, field: field_name
18
- end
19
-
20
- generate_method("move_#{field_name}_to!") do |target_position|
21
- move_field_to target_position, field: field_name
22
- save
23
- end
24
-
25
- generate_method("move_#{field_name}_to=") do |target_position|
26
- move_field_to target_position, field: field_name
27
- end
28
- end
29
-
30
- def generate_insert_at_helpers(field_name)
31
- klass.class_eval do
32
- alias_method "insert_#{field_name}_at!", "move_#{field_name}_to!"
33
- alias_method "insert_#{field_name}_at", "move_#{field_name}_to"
34
- alias_method "insert_#{field_name}_at=", "move_#{field_name}_to="
35
- end
36
- end
37
-
38
- def generate_shorthand_helpers(field_name)
39
- %i[top bottom].each do |symbol|
40
- generate_method "move_#{field_name}_to_#{symbol}" do
41
- move_to symbol, field: field_name
42
- end
43
-
44
- generate_method "move_#{field_name}_to_#{symbol}!" do
45
- move_to! symbol, field: field_name
46
- end
47
- end
48
-
49
- %i[higher lower].each do |symbol|
50
- generate_method "move_#{field_name}_#{symbol}" do
51
- move_to symbol, field: field_name
52
- end
53
-
54
- generate_method "move_#{field_name}_#{symbol}!" do
55
- move_to! symbol, field: field_name
56
- end
57
- end
58
- end
59
- end
60
- end
61
- end
62
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Orderable
5
+ module Generators
6
+ class Movable < Base
7
+ def generate(field_name)
8
+ generate_move_to_helpers(field_name)
9
+ generate_insert_at_helpers(field_name)
10
+ generate_shorthand_helpers(field_name)
11
+ end
12
+
13
+ private
14
+
15
+ def generate_move_to_helpers(field_name)
16
+ generate_method("move_#{field_name}_to") do |target_position|
17
+ move_field_to target_position, field: field_name
18
+ end
19
+
20
+ generate_method("move_#{field_name}_to!") do |target_position|
21
+ move_field_to target_position, field: field_name
22
+ save
23
+ end
24
+
25
+ generate_method("move_#{field_name}_to=") do |target_position|
26
+ move_field_to target_position, field: field_name
27
+ end
28
+ end
29
+
30
+ def generate_insert_at_helpers(field_name)
31
+ klass.class_eval do
32
+ alias_method "insert_#{field_name}_at!", "move_#{field_name}_to!"
33
+ alias_method "insert_#{field_name}_at", "move_#{field_name}_to"
34
+ alias_method "insert_#{field_name}_at=", "move_#{field_name}_to="
35
+ end
36
+ end
37
+
38
+ def generate_shorthand_helpers(field_name)
39
+ %i[top bottom].each do |symbol|
40
+ generate_method "move_#{field_name}_to_#{symbol}" do
41
+ move_to symbol, field: field_name
42
+ end
43
+
44
+ generate_method "move_#{field_name}_to_#{symbol}!" do
45
+ move_to! symbol, field: field_name
46
+ end
47
+ end
48
+
49
+ %i[higher lower].each do |symbol|
50
+ generate_method "move_#{field_name}_#{symbol}" do
51
+ move_to symbol, field: field_name
52
+ end
53
+
54
+ generate_method "move_#{field_name}_#{symbol}!" do
55
+ move_to! symbol, field: field_name
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,26 +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
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
@@ -1,26 +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
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
+ private
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
@@ -1,189 +1,215 @@
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_if,
19
- :orderable_unless,
20
- :orderable_scope,
21
- :orderable_scope_changed?,
22
- :orderable_top,
23
- :orderable_bottom,
24
- :_id,
25
- :new_record?,
26
- :persisted?,
27
- :embedded?,
28
- :collection_name,
29
- to: :doc
30
-
31
- def use_transactions
32
- false
33
- end
34
-
35
- def any_field_changed?
36
- orderable_keys.any? {|field| changed?(field) }
37
- end
38
-
39
- def apply_all_positions
40
- orderable_keys.map {|field| apply_one_position(field, move_all[field]) }
41
- end
42
-
43
- def apply_one_position(field, target_position)
44
- return unless allowed?(field) && changed?(field)
45
-
46
- set_lock(field) if use_transactions
47
-
48
- f = orderable_field(field)
49
- scope = orderable_scope(field)
50
- scope_changed = orderable_scope_changed?(field)
51
-
52
- # Set scope-level lock if scope changed
53
- if use_transactions && persisted? && scope_changed
54
- set_lock(field, true)
55
- scope_changed = orderable_scope_changed?(field)
56
- end
57
-
58
- # Get the current position as exists in the database
59
- current = if !persisted? || scope_changed
60
- nil
61
- elsif persisted? && !embedded?
62
- scope.where(_id: _id).pluck(f).first
63
- else
64
- orderable_position(field)
65
- end
66
-
67
- # If scope changed, remove the position from the old scope
68
- if persisted? && !embedded? && scope_changed
69
- existing_doc = doc.class.unscoped.find(_id)
70
- self.class.new(existing_doc).send(:remove_one_position, field)
71
- end
72
-
73
- # Return if there is no instruction to change the position
74
- in_list = persisted? && current
75
- return if in_list && !target_position
76
-
77
- # Use $inc operator to shift the position of the other documents
78
- target = resolve_target_position(field, target_position, in_list)
79
- if !in_list
80
- scope.gte(f => target).inc(f => 1)
81
- elsif target < current
82
- scope.where(f => { '$gte' => target, '$lt' => current }).inc(f => 1)
83
- elsif target > current
84
- scope.where(f => { '$gt' => current, '$lte' => target }).inc(f => -1)
85
- end
86
-
87
- # If persisted, update the field in the database atomically
88
- doc.set({ f => target }.merge(changed_scope_hash(field))) if use_transactions && persisted?
89
- doc.send("orderable_#{field}_position=", target)
90
- end
91
-
92
- def remove_all_positions
93
- orderable_keys.each do |field|
94
- remove_one_position(field)
95
- end
96
- end
97
-
98
- def remove_one_position(field)
99
- return unless allowed?(field)
100
- f = orderable_field(field)
101
- current = orderable_position(field)
102
- set_lock(field) if use_transactions
103
- orderable_scope(field).gt(f => current).inc(f => -1)
104
- end
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 allowed?(field)
134
- cond_if = orderable_if(field)
135
- cond_unless = orderable_unless(field)
136
-
137
- (cond_if.nil? || resolve_condition(cond_if)) &&
138
- (cond_unless.nil? || !resolve_condition(cond_unless))
139
- end
140
-
141
- def resolve_condition(condition)
142
- case condition
143
- when Proc
144
- condition.arity.zero? ? doc.instance_exec(&condition) : condition.call(doc)
145
- when Symbol
146
- doc.send(condition)
147
- else
148
- condition || false
149
- end
150
- end
151
-
152
- def changed?(field)
153
- return true if new_record? || !doc.send(orderable_field(field)) || move_all[field]
154
- changeable_keys(field).any? {|f| doc.send("#{f}_changed?") }
155
- end
156
-
157
- def changeable_keys(field)
158
- [orderable_field(field)] | scope_keys(field)
159
- end
160
-
161
- def scope_keys(field)
162
- orderable_scope(field).selector.keys.map do |f|
163
- doc.fields[f]&.options&.[](:as) || f
164
- end
165
- end
166
-
167
- def changed_scope_hash(field)
168
- scope_keys(field).each_with_object({}) do |f, hash|
169
- hash[f] = doc.send(f) if doc.send("#{f}_changed?")
170
- end
171
- end
172
-
173
- def set_lock(field, generic = false)
174
- return unless use_transactions
175
- model_name = doc.class.orderable_configs[field][:lock_collection].to_s.singularize.classify
176
- model = Mongoid::Orderable::Models.const_get(model_name)
177
- attrs = lock_scope(field, generic)
178
- model.where(attrs).find_one_and_update(attrs.merge(updated_at: Time.now), { upsert: true })
179
- end
180
-
181
- def lock_scope(field, generic = false)
182
- sel = orderable_scope(field).selector
183
- scope = ([collection_name] + (generic ? [field] : sel.to_a.flatten)).map(&:to_s).join('|')
184
- { scope: scope }
185
- end
186
- end
187
- end
188
- end
189
- end
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
+ reset_new_record
12
+ end
13
+
14
+ private
15
+
16
+ delegate :orderable_keys,
17
+ :orderable_field,
18
+ :orderable_position,
19
+ :orderable_if,
20
+ :orderable_unless,
21
+ :orderable_scope,
22
+ :orderable_scope_changed?,
23
+ :orderable_top,
24
+ :orderable_bottom,
25
+ :_id,
26
+ :persisted?,
27
+ :embedded?,
28
+ :collection_name,
29
+ to: :doc
30
+
31
+ def new_record?
32
+ use_transactions ? @new_record : doc.new_record?
33
+ end
34
+
35
+ def use_transactions
36
+ false
37
+ end
38
+
39
+ def any_field_changed?
40
+ orderable_keys.any? {|field| changed?(field) }
41
+ end
42
+
43
+ def set_new_record_positions
44
+ return unless new_record?
45
+
46
+ orderable_keys.each do |field|
47
+ next unless (position = doc.send(field))
48
+
49
+ move_all[field] ||= position
50
+ end
51
+ end
52
+
53
+ def apply_all_positions
54
+ orderable_keys.each {|field| apply_one_position(field, move_all[field]) }
55
+ end
56
+
57
+ def apply_one_position(field, target_position)
58
+ return unless allowed?(field) && changed?(field)
59
+
60
+ set_lock(field) if use_transactions
61
+
62
+ f = orderable_field(field)
63
+ scope = orderable_scope(field)
64
+ scope_changed = orderable_scope_changed?(field)
65
+
66
+ # Set scope-level lock if scope changed
67
+ if use_transactions && persisted? && scope_changed
68
+ set_lock(field, true)
69
+ scope_changed = orderable_scope_changed?(field)
70
+ end
71
+
72
+ # Get the current position as exists in the database
73
+ current = if new_record? || scope_changed
74
+ nil
75
+ elsif persisted? && !embedded?
76
+ scope.where(_id: _id).pluck(f).first
77
+ else
78
+ orderable_position(field)
79
+ end
80
+
81
+ # If scope changed, remove the position from the old scope
82
+ if persisted? && !embedded? && scope_changed
83
+ existing_doc = doc.class.unscoped.find(_id)
84
+ self.class.new(existing_doc).send(:remove_one_position, field)
85
+ end
86
+
87
+ # Return if there is no instruction to change the position
88
+ in_list = persisted? && current
89
+ return if in_list && !target_position
90
+
91
+ target = resolve_target_position(field, target_position, in_list)
92
+
93
+ # Use $inc operator to shift the position of the other documents
94
+ if !in_list
95
+ scope.gte(f => target).inc(f => 1)
96
+ elsif target < current
97
+ scope.where(f => { '$gte' => target, '$lt' => current }).inc(f => 1)
98
+ elsif target > current
99
+ scope.where(f => { '$gt' => current, '$lte' => target }).inc(f => -1)
100
+ end
101
+
102
+ # If persisted, update the field in the database atomically
103
+ doc.set({ f => target }.merge(changed_scope_hash(field))) if use_transactions && persisted?
104
+ doc.send("orderable_#{field}_position=", target)
105
+ end
106
+
107
+ def remove_all_positions
108
+ orderable_keys.each do |field|
109
+ remove_one_position(field)
110
+ end
111
+ end
112
+
113
+ def remove_one_position(field)
114
+ return unless allowed?(field)
115
+ f = orderable_field(field)
116
+ current = orderable_position(field)
117
+ set_lock(field) if use_transactions
118
+ orderable_scope(field).gt(f => current).inc(f => -1)
119
+ end
120
+
121
+ def move_all
122
+ doc.send(:move_all)
123
+ end
124
+
125
+ def reset
126
+ reset_new_record
127
+ doc.send(:clear_move_all!)
128
+ end
129
+
130
+ # Required for transactions, which perform some actions
131
+ # in the after_create callback.
132
+ def reset_new_record
133
+ @new_record = doc.new_record?
134
+ end
135
+
136
+ def resolve_target_position(field, target_position, in_list)
137
+ target_position ||= 'bottom'
138
+
139
+ unless target_position.is_a?(Numeric)
140
+ target_position = case target_position.to_s
141
+ when 'top' then (min ||= orderable_top(field))
142
+ when 'bottom' then (max ||= orderable_bottom(field, in_list))
143
+ when 'higher' then orderable_position(field).pred
144
+ when 'lower' then orderable_position(field).next
145
+ when /\A\d+\Z/ then target_position.to_i
146
+ else raise Mongoid::Orderable::Errors::InvalidTargetPosition.new(target_position)
147
+ end
148
+ end
149
+
150
+ if target_position <= (min ||= orderable_top(field))
151
+ target_position = min
152
+ elsif target_position > (max ||= orderable_bottom(field, in_list))
153
+ target_position = max
154
+ end
155
+
156
+ target_position
157
+ end
158
+
159
+ def allowed?(field)
160
+ cond_if = orderable_if(field)
161
+ cond_unless = orderable_unless(field)
162
+
163
+ (cond_if.nil? || resolve_condition(cond_if)) &&
164
+ (cond_unless.nil? || !resolve_condition(cond_unless))
165
+ end
166
+
167
+ def resolve_condition(condition)
168
+ case condition
169
+ when Proc
170
+ condition.arity.zero? ? doc.instance_exec(&condition) : condition.call(doc)
171
+ when Symbol
172
+ doc.send(condition)
173
+ else
174
+ condition || false
175
+ end
176
+ end
177
+
178
+ def changed?(field)
179
+ return true if new_record? || !doc.send(orderable_field(field)) || move_all[field]
180
+ changeable_keys(field).any? {|f| doc.send("#{f}_changed?") }
181
+ end
182
+
183
+ def changeable_keys(field)
184
+ [orderable_field(field)] | scope_keys(field)
185
+ end
186
+
187
+ def scope_keys(field)
188
+ orderable_scope(field).selector.keys.map do |f|
189
+ doc.fields[f]&.options&.[](:as) || f
190
+ end
191
+ end
192
+
193
+ def changed_scope_hash(field)
194
+ scope_keys(field).each_with_object({}) do |f, hash|
195
+ hash[f] = doc.send(f) if doc.send("#{f}_changed?")
196
+ end
197
+ end
198
+
199
+ def set_lock(field, generic = false)
200
+ return unless use_transactions
201
+ model_name = doc.class.orderable_configs[field][:lock_collection].to_s.singularize.classify
202
+ model = Mongoid::Orderable::Models.const_get(model_name)
203
+ attrs = lock_scope(field, generic)
204
+ model.where(attrs).find_one_and_update(attrs.merge(updated_at: Time.now), { upsert: true })
205
+ end
206
+
207
+ def lock_scope(field, generic = false)
208
+ sel = orderable_scope(field).selector
209
+ scope = ([collection_name] + (generic ? [field] : sel.to_a.flatten)).map(&:to_s).join('|')
210
+ { scope: scope }
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end