mongoid_orderable 6.0.5 → 6.0.7

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -99
  3. data/LICENSE.txt +20 -20
  4. data/README.md +296 -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 +218 -215
  19. data/lib/mongoid/orderable/handlers/document.rb +32 -32
  20. data/lib/mongoid/orderable/handlers/document_embedded.rb +14 -14
  21. data/lib/mongoid/orderable/handlers/document_transactional.rb +32 -32
  22. data/lib/mongoid/orderable/handlers/transaction.rb +75 -75
  23. data/lib/mongoid/orderable/installer.rb +64 -64
  24. data/lib/mongoid/orderable/mixins/callbacks.rb +45 -45
  25. data/lib/mongoid/orderable/mixins/cascadeable.rb +15 -15
  26. data/lib/mongoid/orderable/mixins/helpers.rb +45 -39
  27. data/lib/mongoid/orderable/mixins/listable.rb +49 -49
  28. data/lib/mongoid/orderable/mixins/movable.rb +64 -64
  29. data/lib/mongoid/orderable/version.rb +7 -7
  30. data/lib/mongoid/orderable.rb +29 -29
  31. data/lib/mongoid_orderable.rb +34 -33
  32. metadata +3 -3
@@ -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
- 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
+ # 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
- 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
+ # 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,215 +1,218 @@
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
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
+ target_position ||= orderable_position(field) || :bottom if embedded? && doc.send("#{f}_changed?")
64
+ scope = orderable_scope(field)
65
+ scope_changed = orderable_scope_changed?(field)
66
+
67
+ # Set scope-level lock if scope changed
68
+ if use_transactions && persisted? && scope_changed
69
+ set_lock(field, true)
70
+ scope_changed = orderable_scope_changed?(field)
71
+ end
72
+
73
+ # Get the current position as exists in the database
74
+ current = if new_record? || scope_changed
75
+ nil
76
+ elsif persisted? && !embedded?
77
+ scope.where(_id: _id).pluck(f).first
78
+ elsif persisted? && embedded?
79
+ doc.send("#{f}_was")
80
+ else
81
+ orderable_position(field)
82
+ end
83
+
84
+ # If scope changed, remove the position from the old scope
85
+ if persisted? && !embedded? && scope_changed
86
+ existing_doc = doc.class.unscoped.find(_id)
87
+ self.class.new(existing_doc).send(:remove_one_position, field)
88
+ end
89
+
90
+ # Return if there is no instruction to change the position
91
+ in_list = persisted? && current
92
+ return if in_list && !target_position
93
+
94
+ target = resolve_target_position(field, target_position, in_list)
95
+
96
+ # Use $inc operator to shift the position of the other documents
97
+ if !in_list
98
+ scope.gte(f => target).inc(f => 1)
99
+ elsif target < current
100
+ scope.where(f => { '$gte' => target, '$lt' => current }).inc(f => 1)
101
+ elsif target > current
102
+ scope.where(f => { '$gt' => current, '$lte' => target }).inc(f => -1)
103
+ end
104
+
105
+ # If persisted, update the field in the database atomically
106
+ doc.set({ f => target }.merge(changed_scope_hash(field))) if use_transactions && persisted?
107
+ doc.send("orderable_#{field}_position=", target)
108
+ end
109
+
110
+ def remove_all_positions
111
+ orderable_keys.each do |field|
112
+ remove_one_position(field)
113
+ end
114
+ end
115
+
116
+ def remove_one_position(field)
117
+ return unless allowed?(field)
118
+ f = orderable_field(field)
119
+ current = orderable_position(field)
120
+ set_lock(field) if use_transactions
121
+ orderable_scope(field).gt(f => current).inc(f => -1)
122
+ end
123
+
124
+ def move_all
125
+ doc.send(:move_all)
126
+ end
127
+
128
+ def reset
129
+ reset_new_record
130
+ doc.send(:clear_move_all!)
131
+ end
132
+
133
+ # Required for transactions, which perform some actions
134
+ # in the after_create callback.
135
+ def reset_new_record
136
+ @new_record = doc.new_record?
137
+ end
138
+
139
+ def resolve_target_position(field, target_position, in_list)
140
+ target_position ||= 'bottom'
141
+
142
+ unless target_position.is_a?(Numeric)
143
+ target_position = case target_position.to_s
144
+ when 'top' then (min ||= orderable_top(field))
145
+ when 'bottom' then (max ||= orderable_bottom(field, in_list))
146
+ when 'higher' then orderable_position(field).pred
147
+ when 'lower' then orderable_position(field).next
148
+ when /\A\d+\Z/ then target_position.to_i
149
+ else raise Mongoid::Orderable::Errors::InvalidTargetPosition.new(target_position)
150
+ end
151
+ end
152
+
153
+ if target_position <= (min ||= orderable_top(field))
154
+ target_position = min
155
+ elsif target_position > (max ||= orderable_bottom(field, in_list))
156
+ target_position = max
157
+ end
158
+
159
+ target_position
160
+ end
161
+
162
+ def allowed?(field)
163
+ cond_if = orderable_if(field)
164
+ cond_unless = orderable_unless(field)
165
+
166
+ (cond_if.nil? || resolve_condition(cond_if)) &&
167
+ (cond_unless.nil? || !resolve_condition(cond_unless))
168
+ end
169
+
170
+ def resolve_condition(condition)
171
+ case condition
172
+ when Proc
173
+ condition.arity.zero? ? doc.instance_exec(&condition) : condition.call(doc)
174
+ when Symbol
175
+ doc.send(condition)
176
+ else
177
+ condition || false
178
+ end
179
+ end
180
+
181
+ def changed?(field)
182
+ return true if new_record? || !doc.send(orderable_field(field)) || move_all[field]
183
+ changeable_keys(field).any? {|f| doc.send("#{f}_changed?") }
184
+ end
185
+
186
+ def changeable_keys(field)
187
+ [orderable_field(field)] | scope_keys(field)
188
+ end
189
+
190
+ def scope_keys(field)
191
+ orderable_scope(field).selector.keys.map do |f|
192
+ doc.fields[f]&.options&.[](:as) || f
193
+ end
194
+ end
195
+
196
+ def changed_scope_hash(field)
197
+ scope_keys(field).each_with_object({}) do |f, hash|
198
+ hash[f] = doc.send(f) if doc.send("#{f}_changed?")
199
+ end
200
+ end
201
+
202
+ def set_lock(field, generic = false)
203
+ return unless use_transactions
204
+ model_name = doc.class.orderable_configs[field][:lock_collection].to_s.singularize.classify
205
+ model = Mongoid::Orderable::Models.const_get(model_name)
206
+ attrs = lock_scope(field, generic)
207
+ model.where(attrs).find_one_and_update(attrs.merge(updated_at: Time.now), { upsert: true })
208
+ end
209
+
210
+ def lock_scope(field, generic = false)
211
+ sel = orderable_scope(field).selector
212
+ scope = ([collection_name] + (generic ? [field] : sel.to_a.flatten)).map(&:to_s).join('|')
213
+ { scope: scope }
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end