all_futures 1.0.3 → 2.0.0
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 +4 -4
- data/Gemfile +4 -1
- data/Gemfile.lock +272 -61
- data/LICENSE.txt +1 -1
- data/README.md +59 -79
- data/Rakefile +13 -1
- data/SUMMARY.md +22 -2
- data/bin/console +8 -0
- data/bin/loc +0 -0
- data/bin/setup +0 -0
- data/bin/standardize +0 -0
- data/lib/all_futures/association.rb +42 -0
- data/lib/all_futures/association_index.rb +69 -0
- data/lib/all_futures/attributes.rb +28 -0
- data/lib/all_futures/base.rb +66 -0
- data/lib/all_futures/callbacks.rb +71 -0
- data/lib/all_futures/configuration.rb +9 -0
- data/lib/all_futures/dirty.rb +116 -0
- data/lib/all_futures/embeds.rb +40 -0
- data/lib/all_futures/errors.rb +37 -0
- data/lib/all_futures/finder.rb +214 -0
- data/lib/all_futures/locale/en.yml +49 -0
- data/lib/all_futures/persistence.rb +595 -0
- data/lib/all_futures/presenter.rb +35 -0
- data/lib/all_futures/railtie.rb +26 -0
- data/lib/all_futures/translation.rb +22 -0
- data/lib/all_futures/validations.rb +10 -0
- data/lib/all_futures/version.rb +3 -1
- data/lib/all_futures/versions.rb +80 -0
- data/lib/all_futures.rb +24 -1
- data/lib/vendor/ACTIVE_ENTITY_MIT-LICENSE +42 -0
- data/lib/vendor/README.md +8 -0
- data/lib/vendor/active_entity/aggregations.rb +277 -0
- data/lib/vendor/active_entity/associations/embeds/association.rb +134 -0
- data/lib/vendor/active_entity/associations/embeds/builder/association.rb +105 -0
- data/lib/vendor/active_entity/associations/embeds/builder/collection_association.rb +71 -0
- data/lib/vendor/active_entity/associations/embeds/builder/embedded_in.rb +38 -0
- data/lib/vendor/active_entity/associations/embeds/builder/embeds_many.rb +13 -0
- data/lib/vendor/active_entity/associations/embeds/builder/embeds_one.rb +16 -0
- data/lib/vendor/active_entity/associations/embeds/builder/singular_association.rb +28 -0
- data/lib/vendor/active_entity/associations/embeds/collection_association.rb +209 -0
- data/lib/vendor/active_entity/associations/embeds/collection_proxy.rb +310 -0
- data/lib/vendor/active_entity/associations/embeds/embedded_in_association.rb +31 -0
- data/lib/vendor/active_entity/associations/embeds/embeds_many_association.rb +15 -0
- data/lib/vendor/active_entity/associations/embeds/embeds_one_association.rb +26 -0
- data/lib/vendor/active_entity/associations/embeds/singular_association.rb +35 -0
- data/lib/vendor/active_entity/associations.rb +168 -0
- data/lib/vendor/active_entity/attribute_assignment.rb +87 -0
- data/lib/vendor/active_entity/attribute_decorators.rb +90 -0
- data/lib/vendor/active_entity/attribute_methods/before_type_cast.rb +78 -0
- data/lib/vendor/active_entity/attribute_methods/dirty.rb +180 -0
- data/lib/vendor/active_entity/attribute_methods/primary_key.rb +96 -0
- data/lib/vendor/active_entity/attribute_methods/query.rb +38 -0
- data/lib/vendor/active_entity/attribute_methods/read.rb +44 -0
- data/lib/vendor/active_entity/attribute_methods/serialization.rb +90 -0
- data/lib/vendor/active_entity/attribute_methods/time_zone_conversion.rb +93 -0
- data/lib/vendor/active_entity/attribute_methods/write.rb +54 -0
- data/lib/vendor/active_entity/attribute_methods.rb +833 -0
- data/lib/vendor/active_entity/attributes.rb +240 -0
- data/lib/vendor/active_entity/base.rb +293 -0
- data/lib/vendor/active_entity/callbacks.rb +21 -0
- data/lib/vendor/active_entity/coders/json.rb +15 -0
- data/lib/vendor/active_entity/coders/yaml_column.rb +50 -0
- data/lib/vendor/active_entity/core.rb +348 -0
- data/lib/vendor/active_entity/enum.rb +260 -0
- data/lib/vendor/active_entity/errors.rb +69 -0
- data/lib/vendor/active_entity/gem_version.rb +17 -0
- data/lib/vendor/active_entity/inheritance.rb +176 -0
- data/lib/vendor/active_entity/integration.rb +78 -0
- data/lib/vendor/active_entity/locale/en.yml +45 -0
- data/lib/vendor/active_entity/model_schema.rb +118 -0
- data/lib/vendor/active_entity/nested_attributes.rb +554 -0
- data/lib/vendor/active_entity/persistence.rb +20 -0
- data/lib/vendor/active_entity/railtie.rb +76 -0
- data/lib/vendor/active_entity/readonly_attributes.rb +55 -0
- data/lib/vendor/active_entity/reflection.rb +447 -0
- data/lib/vendor/active_entity/serialization.rb +28 -0
- data/lib/vendor/active_entity/store.rb +291 -0
- data/lib/vendor/active_entity/translation.rb +24 -0
- data/lib/vendor/active_entity/type/date.rb +9 -0
- data/lib/vendor/active_entity/type/date_time.rb +9 -0
- data/lib/vendor/active_entity/type/internal/timezone.rb +17 -0
- data/lib/vendor/active_entity/type/json.rb +30 -0
- data/lib/vendor/active_entity/type/modifiers/array.rb +72 -0
- data/lib/vendor/active_entity/type/modifiers/array_without_blank.rb +19 -0
- data/lib/vendor/active_entity/type/registry.rb +112 -0
- data/lib/vendor/active_entity/type/serialized.rb +71 -0
- data/lib/vendor/active_entity/type/text.rb +11 -0
- data/lib/vendor/active_entity/type/time.rb +21 -0
- data/lib/vendor/active_entity/type/unsigned_integer.rb +17 -0
- data/lib/vendor/active_entity/type.rb +75 -0
- data/lib/vendor/active_entity/validate_embeds_association.rb +300 -0
- data/lib/vendor/active_entity/validations/absence.rb +25 -0
- data/lib/vendor/active_entity/validations/associated.rb +60 -0
- data/lib/vendor/active_entity/validations/length.rb +26 -0
- data/lib/vendor/active_entity/validations/presence.rb +68 -0
- data/lib/vendor/active_entity/validations/subset.rb +76 -0
- data/lib/vendor/active_entity/validations/uniqueness_in_embeds.rb +91 -0
- data/lib/vendor/active_entity/validations/uniqueness_on_active_record.rb +234 -0
- data/lib/vendor/active_entity/validations.rb +47 -0
- data/lib/vendor/active_entity/version.rb +10 -0
- data/lib/vendor/active_entity.rb +99 -0
- data/lib/vendor/core_ext/array_without_blank.rb +48 -0
- metadata +175 -21
- data/lib/all_futures/all_futures.rb +0 -28
- data/lib/all_futures/config.rb +0 -14
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
module Persistence
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
def decrement(attribute, by = 1)
|
|
8
|
+
increment attribute, -by
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def decrement!(attribute, by = 1)
|
|
12
|
+
increment! attribute, -by
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def delete
|
|
16
|
+
catch(:abort) do
|
|
17
|
+
_destroy_associations
|
|
18
|
+
_destroy_association_indexes
|
|
19
|
+
_remove_from_parent_indexes!
|
|
20
|
+
_delete_record if persisted?
|
|
21
|
+
@destroyed = true
|
|
22
|
+
return freeze
|
|
23
|
+
end
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def destroy
|
|
28
|
+
_raise_readonly_record_error if readonly?
|
|
29
|
+
delete
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def destroy!
|
|
33
|
+
_raise_readonly_record_error if readonly?
|
|
34
|
+
delete || _raise_record_not_destroyed_error
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def destroyed?
|
|
38
|
+
@destroyed
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def increment(attribute, by = 1)
|
|
42
|
+
_raise_invalid_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
43
|
+
self[attribute] ||= 0
|
|
44
|
+
self[attribute] += by
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def increment!(attribute, by = 1)
|
|
49
|
+
increment attribute, by
|
|
50
|
+
save
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def new_record?
|
|
55
|
+
@new_record
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def previously_new_record?
|
|
59
|
+
@previously_new_record
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def persisted?
|
|
63
|
+
!(@new_record || @destroyed)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def reload
|
|
67
|
+
raise AllFutures::RecordNotSaved.new("Can't load model that hasn't been saved") unless persisted?
|
|
68
|
+
record = self.class.send(:load_model, id)
|
|
69
|
+
attributes.each do |key, value|
|
|
70
|
+
self[key] = record["attributes"][key]
|
|
71
|
+
end
|
|
72
|
+
@new_record = false
|
|
73
|
+
@previously_new_record = false
|
|
74
|
+
if versioning_enabled?
|
|
75
|
+
@_current_version = record["current_version"]
|
|
76
|
+
@_versions = record["versions"].transform_keys(&:to_i)
|
|
77
|
+
end
|
|
78
|
+
changes_applied # reloaded values are not user-supplied changes; keeps future dirty tracking alive
|
|
79
|
+
instance_variable_set :@updated_at, Time.zone.parse(record["updated_at"])
|
|
80
|
+
instance_variable_set :@marked_for_destruction, false
|
|
81
|
+
instance_variable_set :@destroyed_by_association, nil
|
|
82
|
+
self.class.send(:set_previous_attributes, self, record)
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def save
|
|
87
|
+
_create_or_update
|
|
88
|
+
rescue AllFutures::RecordInvalid
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def save!
|
|
93
|
+
_create_or_update || _raise_record_not_saved_error
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def toggle(attribute)
|
|
97
|
+
_raise_invalid_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
98
|
+
self[attribute] = !public_send("#{attribute}?")
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def toggle!(attribute)
|
|
103
|
+
_raise_invalid_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
104
|
+
toggle attribute
|
|
105
|
+
update_attribute attribute, self[attribute]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def update(attrs)
|
|
109
|
+
assign_attributes attrs
|
|
110
|
+
save
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def update!(attrs)
|
|
114
|
+
assign_attributes attrs
|
|
115
|
+
save!
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def update_attribute(attribute, value)
|
|
119
|
+
_raise_invalid_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
120
|
+
_raise_readonly_attribute_error(attribute) if attr_readonly_enabled? && readonly_attribute?(attribute) && attribute_will_change?(attribute)
|
|
121
|
+
write_attribute attribute, value
|
|
122
|
+
|
|
123
|
+
touch
|
|
124
|
+
|
|
125
|
+
save
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def touch
|
|
129
|
+
@updated_at = Time.current
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# treat as private
|
|
133
|
+
|
|
134
|
+
def _changed_for_autosave?
|
|
135
|
+
new_record? || has_changes_to_save? || marked_for_destruction? || _nested_records_changed_for_autosave?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def _nested_records_changed_for_autosave?
|
|
139
|
+
@_nested_records_changed_for_autosave_already_called ||= false
|
|
140
|
+
return false if @_nested_records_changed_for_autosave_already_called
|
|
141
|
+
begin
|
|
142
|
+
@_nested_records_changed_for_autosave_already_called = true
|
|
143
|
+
_reflections.values.any? do |reflection|
|
|
144
|
+
if reflection.options[:autosave]
|
|
145
|
+
association = association_instance_get(reflection.name)
|
|
146
|
+
association && Array.wrap(association.target).any?(&:_changed_for_autosave?)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
ensure
|
|
150
|
+
@_nested_records_changed_for_autosave_already_called = false
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def _association_foreign_key_changed?(reflection, record, key)
|
|
157
|
+
record._has_attribute?(reflection.options[:foreign_key]) && record._read_attribute(reflection.options[:foreign_key]) != key
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def _associated_records_to_validate_or_save(association, new_record, autosave)
|
|
161
|
+
if new_record || _custom_validation_context?
|
|
162
|
+
association&.target
|
|
163
|
+
elsif autosave
|
|
164
|
+
association.target.find_all(&:_changed_for_autosave?)
|
|
165
|
+
else
|
|
166
|
+
association.target.find_all(&:new_record?)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def _association_valid?(reflection, record, index = nil)
|
|
171
|
+
return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?)
|
|
172
|
+
|
|
173
|
+
context = validation_context if _custom_validation_context?
|
|
174
|
+
|
|
175
|
+
unless (valid = record.valid?(context))
|
|
176
|
+
if reflection.options[:autosave]
|
|
177
|
+
record.errors.group_by_attribute.each do |attribute, errors|
|
|
178
|
+
errors.each do |error|
|
|
179
|
+
self.errors.import(error, attribute: "#{reflection.name}.#{attribute}")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
else
|
|
183
|
+
errors.add(reflection.name)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
valid
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def _create_or_update
|
|
190
|
+
_raise_readonly_record_error if readonly?
|
|
191
|
+
attributes.each_key { |attribute| _raise_readonly_attribute_error(attribute) if attr_readonly_enabled? && readonly_attribute?(attribute) && attribute_will_change?(attribute) }
|
|
192
|
+
return false if destroyed?
|
|
193
|
+
|
|
194
|
+
# capture before changes_applied resets dirty tracking; _save_record uses
|
|
195
|
+
# this to skip the Redis write (and version bump) when nothing changed
|
|
196
|
+
@_pending_write = new_record? || dirty?
|
|
197
|
+
|
|
198
|
+
changes_applied
|
|
199
|
+
|
|
200
|
+
previously_new_record_before_save = (@new_record_before_save ||= false)
|
|
201
|
+
@new_record_before_save = !previously_new_record_before_save && new_record?
|
|
202
|
+
|
|
203
|
+
_reflections.values.select { |reflection| reflection.macro == :embedded_in }.each do |reflection|
|
|
204
|
+
send(:_save_embedded_in, reflection)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
result = new_record? ? _create_record : _update_record
|
|
208
|
+
|
|
209
|
+
_reflections.values.reject { |reflection| reflection.macro == :embedded_in }.each do |reflection|
|
|
210
|
+
send("_save_#{reflection.macro}", reflection)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
@new_record_before_save = previously_new_record_before_save
|
|
214
|
+
|
|
215
|
+
result != false
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def _create_record
|
|
219
|
+
_save_record
|
|
220
|
+
@new_record = false
|
|
221
|
+
@previously_new_record = true
|
|
222
|
+
_sync_embed_indexes!
|
|
223
|
+
true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def _custom_validation_context?
|
|
227
|
+
validation_context && [:create, :update].exclude?(validation_context)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def _delete_record
|
|
231
|
+
Kredis.redis.del(@redis_key)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def _destroy_associations
|
|
235
|
+
_reflections.values.each do |reflection|
|
|
236
|
+
if reflection.options[:dependent]
|
|
237
|
+
association = association_instance_get(reflection.name)
|
|
238
|
+
next unless association
|
|
239
|
+
|
|
240
|
+
case reflection.options[:dependent]
|
|
241
|
+
when :delete
|
|
242
|
+
Array.wrap(association.target).each(&:delete)
|
|
243
|
+
when :destroy
|
|
244
|
+
Array.wrap(association.target).each do |record|
|
|
245
|
+
record.destroyed_by_association = reflection
|
|
246
|
+
record.destroy
|
|
247
|
+
end
|
|
248
|
+
when :nullify
|
|
249
|
+
raise AllFutures::InvalidDependentOption.new(:nullify) if reflection.macro == :embedded_in
|
|
250
|
+
Array.wrap(association.target).each do |record|
|
|
251
|
+
record.update_attribute(reflection.options[:foreign_key], nil) if record.persisted?
|
|
252
|
+
end
|
|
253
|
+
when :restrict_with_exception
|
|
254
|
+
raise AllFutures::InvalidDependentOption.new(:restrict_with_exception) if reflection.macro == :embedded_in
|
|
255
|
+
raise AllFutures::DeleteRestrictionError.new(reflection.name) unless association.empty?
|
|
256
|
+
when :restrict_with_error
|
|
257
|
+
raise AllFutures::InvalidDependentOption.new(:restrict_with_error) if reflection.macro == :embedded_in
|
|
258
|
+
unless association.empty?
|
|
259
|
+
record = association.owner.class.human_attribute_name(reflection.name).downcase
|
|
260
|
+
association.owner.errors.add(:base, :"restrict_dependent_destroy.#{reflection.macro}", record: record)
|
|
261
|
+
throw(:abort)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def _record_changed?(reflection, record, key)
|
|
269
|
+
record.new_record? ||
|
|
270
|
+
_association_foreign_key_changed?(reflection, record, key) ||
|
|
271
|
+
record.will_save_change_to_attribute?(reflection.options[:foreign_key])
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def _save_embeds_many(reflection)
|
|
275
|
+
if (association = association_instance_get(reflection.name))
|
|
276
|
+
_raise_missing_foreign_key_error(reflection) unless reflection.klass.has_attribute?(reflection.options[:foreign_key])
|
|
277
|
+
autosave = reflection.options[:autosave]
|
|
278
|
+
|
|
279
|
+
new_record_before_save = @new_record_before_save
|
|
280
|
+
|
|
281
|
+
if (records = _associated_records_to_validate_or_save(association, new_record_before_save, autosave))
|
|
282
|
+
if autosave
|
|
283
|
+
records_to_destroy = records.select(&:marked_for_destruction?)
|
|
284
|
+
records_to_destroy.each { |record| association.destroy(record) }
|
|
285
|
+
records -= records_to_destroy
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
records.each do |record|
|
|
289
|
+
next if record.destroyed?
|
|
290
|
+
|
|
291
|
+
saved = true
|
|
292
|
+
if autosave != false && (new_record_before_save || record.new_record?)
|
|
293
|
+
association.set_inverse_instance(record)
|
|
294
|
+
|
|
295
|
+
record.without_versioning do
|
|
296
|
+
if autosave
|
|
297
|
+
record._write_attribute(reflection.options[:foreign_key], id)
|
|
298
|
+
saved = record.save
|
|
299
|
+
elsif !reflection.nested?
|
|
300
|
+
record._write_attribute(reflection.options[:foreign_key], id)
|
|
301
|
+
association_saved = record.save
|
|
302
|
+
|
|
303
|
+
if reflection.validate?
|
|
304
|
+
errors.add(reflection.name) unless association_saved
|
|
305
|
+
saved = association_saved
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
elsif autosave
|
|
310
|
+
saved = record.save
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
raise(AllFutures::RecordInvalid.new(association.owner)) unless saved
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def _save_embeds_one(reflection)
|
|
320
|
+
association = association_instance_get(reflection.name)
|
|
321
|
+
_raise_missing_foreign_key_error(reflection) unless reflection.klass.has_attribute?(reflection.options[:foreign_key])
|
|
322
|
+
|
|
323
|
+
record = association&.target
|
|
324
|
+
|
|
325
|
+
if record && !record.destroyed?
|
|
326
|
+
autosave = reflection.options[:autosave]
|
|
327
|
+
if autosave && record.marked_for_destruction?
|
|
328
|
+
record.destroy
|
|
329
|
+
elsif autosave != false
|
|
330
|
+
if (autosave && record._changed_for_autosave?) || _record_changed?(reflection, record, id)
|
|
331
|
+
record._write_attribute(reflection.options[:foreign_key], id)
|
|
332
|
+
association.set_inverse_instance(record)
|
|
333
|
+
record.without_versioning { record.save }
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def _save_embedded_in(reflection)
|
|
340
|
+
association = association_instance_get(reflection.name)
|
|
341
|
+
return unless association&.loaded?
|
|
342
|
+
|
|
343
|
+
record = association.target
|
|
344
|
+
|
|
345
|
+
if record && !record.destroyed?
|
|
346
|
+
autosave = reflection.options[:autosave]
|
|
347
|
+
|
|
348
|
+
if autosave && record.marked_for_destruction?
|
|
349
|
+
record._write_attribute(reflection.options[:foreign_key], nil)
|
|
350
|
+
record.destroy
|
|
351
|
+
elsif autosave != false
|
|
352
|
+
saved = record.save if record.new_record? || (autosave && record.changed_for_autosave?)
|
|
353
|
+
|
|
354
|
+
saved if autosave
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Redis Lua: compare current_version then SET, or return STALE.
|
|
360
|
+
# KEYS[1] = redis key, ARGV[1] = expected version ("" if new), ARGV[2] = JSON payload
|
|
361
|
+
ATOMIC_SAVE_LUA = <<~LUA.freeze
|
|
362
|
+
local key = KEYS[1]
|
|
363
|
+
local expected = ARGV[1]
|
|
364
|
+
local payload = ARGV[2]
|
|
365
|
+
local raw = redis.call('GET', key)
|
|
366
|
+
|
|
367
|
+
if not raw then
|
|
368
|
+
if expected == '' then
|
|
369
|
+
redis.call('SET', key, payload)
|
|
370
|
+
return 'OK'
|
|
371
|
+
end
|
|
372
|
+
return 'STALE'
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
local data = cjson.decode(raw)
|
|
376
|
+
local current = data['current_version']
|
|
377
|
+
if current == cjson.null or current == nil then
|
|
378
|
+
current = ''
|
|
379
|
+
else
|
|
380
|
+
current = tostring(current)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
if current == expected then
|
|
384
|
+
redis.call('SET', key, payload)
|
|
385
|
+
return 'OK'
|
|
386
|
+
end
|
|
387
|
+
return 'STALE'
|
|
388
|
+
LUA
|
|
389
|
+
|
|
390
|
+
def _save_record
|
|
391
|
+
return true unless @_pending_write
|
|
392
|
+
|
|
393
|
+
if versioning_enabled? && AllFutures.atomic_locking
|
|
394
|
+
_save_record_atomically
|
|
395
|
+
else
|
|
396
|
+
_save_version if versioning_enabled?
|
|
397
|
+
touch
|
|
398
|
+
Kredis.json(@redis_key).value = _snapshot
|
|
399
|
+
end
|
|
400
|
+
true
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def _save_record_atomically
|
|
404
|
+
expected = @_current_version
|
|
405
|
+
previous_version = @_current_version
|
|
406
|
+
previous_versions = @_versions.dup
|
|
407
|
+
|
|
408
|
+
if new_record?
|
|
409
|
+
@_current_version = 1
|
|
410
|
+
else
|
|
411
|
+
@_current_version = @_current_version ? @_current_version.next : 1
|
|
412
|
+
end
|
|
413
|
+
@_versions[current_version] = {
|
|
414
|
+
"attributes" => attributes,
|
|
415
|
+
"updated_at" => Time.current
|
|
416
|
+
}
|
|
417
|
+
touch
|
|
418
|
+
payload = ActiveSupport::JSON.encode(_snapshot)
|
|
419
|
+
expected_arg = expected.nil? ? "" : expected.to_s
|
|
420
|
+
|
|
421
|
+
result = Kredis.redis.eval(ATOMIC_SAVE_LUA, keys: [@redis_key], argv: [expected_arg, payload])
|
|
422
|
+
if result == "STALE"
|
|
423
|
+
@_current_version = previous_version
|
|
424
|
+
@_versions = previous_versions
|
|
425
|
+
_raise_record_stale_error
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def _sync_embed_indexes!
|
|
430
|
+
_reflections.values.select { |reflection| reflection.macro == :embedded_in }.each do |reflection|
|
|
431
|
+
inverse = reflection.inverse_of
|
|
432
|
+
next unless inverse
|
|
433
|
+
|
|
434
|
+
parent_class = reflection.klass
|
|
435
|
+
next unless parent_class < AllFutures::Base
|
|
436
|
+
|
|
437
|
+
fk = reflection.options[:foreign_key]
|
|
438
|
+
current_parent_id = _read_attribute(fk)
|
|
439
|
+
previous_parent_id = begin
|
|
440
|
+
attribute_previously_was(fk)
|
|
441
|
+
rescue
|
|
442
|
+
nil
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
if previous_parent_id.present? && previous_parent_id.to_s != current_parent_id.to_s
|
|
446
|
+
AssociationIndex.remove(parent_class, previous_parent_id, inverse.name, id)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
if current_parent_id.present?
|
|
450
|
+
AssociationIndex.add(parent_class, current_parent_id, inverse.name, id)
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def _remove_from_parent_indexes!
|
|
456
|
+
_reflections.values.select { |reflection| reflection.macro == :embedded_in }.each do |reflection|
|
|
457
|
+
inverse = reflection.inverse_of
|
|
458
|
+
next unless inverse
|
|
459
|
+
|
|
460
|
+
parent_class = reflection.klass
|
|
461
|
+
next unless parent_class < AllFutures::Base
|
|
462
|
+
|
|
463
|
+
parent_id = _read_attribute(reflection.options[:foreign_key])
|
|
464
|
+
AssociationIndex.remove(parent_class, parent_id, inverse.name, id) if parent_id.present?
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def _destroy_association_indexes
|
|
469
|
+
return unless id
|
|
470
|
+
|
|
471
|
+
_reflections.values.select { |reflection| [:embeds_many, :embeds_one].include?(reflection.macro) }.each do |reflection|
|
|
472
|
+
AssociationIndex.clear(self.class, id, reflection.name)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def _save_version
|
|
477
|
+
if new_record?
|
|
478
|
+
@_current_version = 1
|
|
479
|
+
else
|
|
480
|
+
record = Kredis.json(@redis_key).value
|
|
481
|
+
_raise_record_stale_error if record&.dig("current_version") != @_current_version
|
|
482
|
+
@_current_version = @_current_version ? @_current_version.next : 1
|
|
483
|
+
end
|
|
484
|
+
@_versions[current_version] = {
|
|
485
|
+
"attributes" => attributes,
|
|
486
|
+
"updated_at" => Time.current
|
|
487
|
+
}
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def _snapshot
|
|
491
|
+
{
|
|
492
|
+
attributes: attributes,
|
|
493
|
+
created_at: created_at,
|
|
494
|
+
updated_at: @updated_at,
|
|
495
|
+
previous_attributes: previous_attributes,
|
|
496
|
+
current_version: current_version,
|
|
497
|
+
versions: versions.transform_keys(&:to_i)
|
|
498
|
+
}.deep_transform_keys { |key| key.is_a?(Integer) ? key : key.to_sym }
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def _raise_invalid_attribute_error(attribute)
|
|
502
|
+
raise AllFutures::InvalidAttribute.new(self, attribute)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def _raise_missing_foreign_key_error(reflection)
|
|
506
|
+
raise AllFutures::MissingForeignKeyError, "#{reflection.klass} missing foreign key #{reflection.options[:foreign_key]}"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def _raise_readonly_attribute_error(attribute)
|
|
510
|
+
raise AllFutures::ReadOnlyRecord, "#{attribute} is marked as readonly"
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def _raise_readonly_record_error
|
|
514
|
+
raise AllFutures::ReadOnlyRecord, "#{self.class} is marked as readonly"
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def _raise_record_not_destroyed_error
|
|
518
|
+
raise AllFutures::RecordNotDestroyed, "Failed to destroy the record"
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def _raise_record_not_saved_error
|
|
522
|
+
raise AllFutures::RecordNotSaved, "Failed to save the record"
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def _raise_record_stale_error
|
|
526
|
+
raise AllFutures::RecordStale, "Failed to save the record because it has been modified by another process"
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def _update_record
|
|
530
|
+
_save_record
|
|
531
|
+
@previously_new_record = false
|
|
532
|
+
_sync_embed_indexes!
|
|
533
|
+
true
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def _validate_single_association(reflection)
|
|
537
|
+
association = association_instance_get(reflection.name)
|
|
538
|
+
record = association&.reader
|
|
539
|
+
_association_valid?(reflection, record) if record && (record._changed_for_autosave? || _custom_validation_context?)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def _validate_collection_association(reflection)
|
|
543
|
+
if (association = association_instance_get(reflection.name))
|
|
544
|
+
if (records = _associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]))
|
|
545
|
+
records.each_with_index { |record, index| _association_valid?(reflection, record, index) }
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
module ClassMethods
|
|
551
|
+
def create(attributes = {}, &block)
|
|
552
|
+
new(attributes).tap do |record|
|
|
553
|
+
block&.call(record)
|
|
554
|
+
record.save
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def delete_all
|
|
559
|
+
Kredis.redis.del Kredis.redis.keys("#{name}:*")
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def delete_by(attributes = {}, &block)
|
|
563
|
+
Kredis.redis.del where(attributes, &block).map { |record| "#{name}:#{record.id}" }
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def destroy_all
|
|
567
|
+
all.each(&:destroy)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def destroy_by(attributes = {}, &block)
|
|
571
|
+
where(attributes, &block).each(&:destroy)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def readonly_attribute?(name)
|
|
575
|
+
_attr_readonly.include?(name.to_s)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
private
|
|
579
|
+
|
|
580
|
+
def load_model(id)
|
|
581
|
+
record = Kredis.json("#{name}:#{id}").value
|
|
582
|
+
raise AllFutures::RecordNotFound.new("Couldn't find #{name} with id #{id}") unless record
|
|
583
|
+
attribute_names.each do |attribute|
|
|
584
|
+
case attribute_types[attribute]
|
|
585
|
+
when ActiveEntity::Type::Date
|
|
586
|
+
record[attribute] = Date.parse(record[attribute]) if record[attribute]
|
|
587
|
+
when ActiveEntity::Type::DateTime, ActiveEntity::Type::Time
|
|
588
|
+
record[attribute] = Time.zone.parse(record[attribute]) if record[attribute]
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
record
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
module Presenter
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
attr_reader :updated_at
|
|
8
|
+
|
|
9
|
+
def max_updated_column_timestamp
|
|
10
|
+
@updated_at&.to_time
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def reject
|
|
14
|
+
attributes
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_dom_id
|
|
18
|
+
[self.class.name.underscore.dasherize.gsub("/", "--"), id].join("-")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
attributes
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
inspect
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module ClassMethods
|
|
30
|
+
def valid_attribute?(attribute)
|
|
31
|
+
(attribute_names + ["id"]).include?(attribute.to_s)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AllFutures::Railtie < ::Rails::Railtie
|
|
4
|
+
config.all_futures = ActiveSupport::OrderedOptions.new
|
|
5
|
+
config.all_futures.atomic_locking = false
|
|
6
|
+
|
|
7
|
+
initializer "all_futures.configure" do |app|
|
|
8
|
+
AllFutures.atomic_locking = app.config.all_futures.atomic_locking
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer "all_futures.attributes" do
|
|
12
|
+
config.after_initialize do
|
|
13
|
+
ActiveModel::Model.include AllFutures::Attributes if defined?(ActiveModel::Model)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
ActiveSupport.on_load(:active_record) do
|
|
17
|
+
include AllFutures::Attributes
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer "all_futures.i18n" do
|
|
22
|
+
ActiveSupport.on_load(:i18n) do
|
|
23
|
+
I18n.load_path << File.expand_path("locale/en.yml", __dir__)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
module Translation
|
|
5
|
+
include ActiveModel::Translation
|
|
6
|
+
|
|
7
|
+
def lookup_ancestors
|
|
8
|
+
klass = self
|
|
9
|
+
classes = [klass]
|
|
10
|
+
return classes if klass == AllFutures::Base
|
|
11
|
+
|
|
12
|
+
until klass.base_class?
|
|
13
|
+
classes << klass = klass.superclass
|
|
14
|
+
end
|
|
15
|
+
classes
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def i18n_scope
|
|
19
|
+
:allfutures
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/all_futures/version.rb
CHANGED