all_futures 1.0.2 → 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 +297 -0
- data/LICENSE.txt +1 -1
- data/README.md +59 -77
- 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 +172 -31
- data/lib/all_futures/all_futures.rb +0 -28
- data/lib/all_futures/config.rb +0 -14
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
# Maintains Redis SETs that index embedded children by owner so associations
|
|
5
|
+
# can be lazy-loaded after find without scanning every key of the child class.
|
|
6
|
+
#
|
|
7
|
+
# Key shape: "OwnerClass:owner_id:association_name" => SET of child ids
|
|
8
|
+
module AssociationIndex
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def key(owner_class, owner_id, association_name)
|
|
12
|
+
"#{owner_class.name}:#{owner_id}:#{association_name}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add(owner_class, owner_id, association_name, child_id)
|
|
16
|
+
return if owner_id.blank? || child_id.blank?
|
|
17
|
+
Kredis.redis.sadd(key(owner_class, owner_id, association_name), child_id.to_s)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def remove(owner_class, owner_id, association_name, child_id)
|
|
21
|
+
return if owner_id.blank? || child_id.blank?
|
|
22
|
+
Kredis.redis.srem(key(owner_class, owner_id, association_name), child_id.to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def clear(owner_class, owner_id, association_name)
|
|
26
|
+
return if owner_id.blank?
|
|
27
|
+
Kredis.redis.del(key(owner_class, owner_id, association_name))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def member_ids(owner_class, owner_id, association_name)
|
|
31
|
+
return [] if owner_id.blank?
|
|
32
|
+
Array(Kredis.redis.smembers(key(owner_class, owner_id, association_name)))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def hydrate(association)
|
|
36
|
+
return if association.instance_variable_get(:@af_hydrated)
|
|
37
|
+
association.instance_variable_set(:@af_hydrated, true)
|
|
38
|
+
|
|
39
|
+
owner = association.owner
|
|
40
|
+
return unless owner.is_a?(AllFutures::Base) && owner.persisted?
|
|
41
|
+
|
|
42
|
+
reflection = association.reflection
|
|
43
|
+
return unless [:embeds_many, :embeds_one].include?(reflection.macro)
|
|
44
|
+
|
|
45
|
+
ids = member_ids(owner.class, owner.id, reflection.name)
|
|
46
|
+
return if ids.empty?
|
|
47
|
+
|
|
48
|
+
existing_ids = Array.wrap(association.target).compact.map { |r| r.id.to_s }
|
|
49
|
+
|
|
50
|
+
ids.each do |child_id|
|
|
51
|
+
next if existing_ids.include?(child_id.to_s)
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
record = reflection.klass.find(child_id)
|
|
55
|
+
rescue AllFutures::RecordNotFound
|
|
56
|
+
remove(owner.class, owner.id, reflection.name, child_id)
|
|
57
|
+
next
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if reflection.macro == :embeds_many
|
|
61
|
+
association.add_to_target(record, true)
|
|
62
|
+
else
|
|
63
|
+
association.target = record
|
|
64
|
+
association.set_inverse_instance(record)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
module Attributes
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def has_future(name, klass, **options)
|
|
9
|
+
ivar_symbol = :"@#{name}_all_futures"
|
|
10
|
+
|
|
11
|
+
define_method(name) do
|
|
12
|
+
if instance_variable_defined?(ivar_symbol)
|
|
13
|
+
instance_variable_get(ivar_symbol)
|
|
14
|
+
else
|
|
15
|
+
af_key = if options[:key]
|
|
16
|
+
options[:key]
|
|
17
|
+
else
|
|
18
|
+
record_id = try(:id) or raise AllFutures::ParentModelNotSavedYet, "AllFutures requires a unique key. Either save the parent model before accessing #{name}, or pass a custom key."
|
|
19
|
+
"#{self.class.name.tableize.tr("/", ":")}:#{record_id}:#{name}"
|
|
20
|
+
end
|
|
21
|
+
af = klass.exists?(af_key) ? klass.find(af_key) : klass.new(id: af_key)
|
|
22
|
+
instance_variable_set(ivar_symbol, af)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
class Base < ActiveEntity::Base
|
|
5
|
+
prepend ::AllFutures::Callbacks
|
|
6
|
+
extend ::ActiveModel::Naming
|
|
7
|
+
extend ::AllFutures::Translation
|
|
8
|
+
include ::AllFutures::Persistence
|
|
9
|
+
include ::AllFutures::Dirty
|
|
10
|
+
include ::AllFutures::Validations
|
|
11
|
+
include ::AllFutures::Presenter
|
|
12
|
+
include ::AllFutures::Versions
|
|
13
|
+
include ::AllFutures::Finder
|
|
14
|
+
include ::AllFutures::Embeds
|
|
15
|
+
include ::ActiveModel::Conversion
|
|
16
|
+
include ::ActiveModel::SecurePassword
|
|
17
|
+
include ::ActiveRecord::Integration
|
|
18
|
+
include ::Kredis::Attributes
|
|
19
|
+
|
|
20
|
+
attr_reader :created_at, :updated_at
|
|
21
|
+
|
|
22
|
+
def initialize(attributes = {}, &block)
|
|
23
|
+
attributes ||= {}
|
|
24
|
+
attributes = attributes.attributes.transform_keys(&:to_sym) if attributes.is_a?(ActiveRecord::Base)
|
|
25
|
+
attributes_for_super = attributes.key?(:id) ? attributes.except(:id).except(:created_at).except(:updated_at) : attributes
|
|
26
|
+
|
|
27
|
+
super(attributes_for_super) do
|
|
28
|
+
@id = attributes&.fetch(:id, nil) || ULID.generate
|
|
29
|
+
@created_at = Time.current
|
|
30
|
+
@updated_at = attributes&.fetch(:updated_at, Time.current)
|
|
31
|
+
@redis_key = "#{self.class.name}:#{@id}"
|
|
32
|
+
@new_record = !self.class.exists?(@id)
|
|
33
|
+
@destroyed = false
|
|
34
|
+
@previously_new_record = false
|
|
35
|
+
@_versioning_enabled = self.class.versioning
|
|
36
|
+
@_versions = {}
|
|
37
|
+
@_current_version = nil
|
|
38
|
+
@_id_set = attributes.key?(:id)
|
|
39
|
+
|
|
40
|
+
@attributes.keys.each do |attr|
|
|
41
|
+
define_singleton_method("#{attr}_changed?") { attribute_changed?(attr) }
|
|
42
|
+
define_singleton_method("#{attr}_valid?") { attribute_valid?(attr) }
|
|
43
|
+
define_singleton_method("rollback_#{attr}") { rollback_attribute(attr) }
|
|
44
|
+
define_singleton_method("rollback_#{attr}!") { rollback_attribute!(attr) }
|
|
45
|
+
define_singleton_method("restore_#{attr}") { restore_attribute(attr) }
|
|
46
|
+
define_singleton_method("restore_#{attr}!") { restore_attribute(attr) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
block&.call(self)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def id
|
|
54
|
+
(!new_record? || @_id_set) ? @id.to_s : nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def id=(value)
|
|
58
|
+
raise FrozenError.new("can't modify id when persisted") unless new_record?
|
|
59
|
+
@id = value.to_s
|
|
60
|
+
@redis_key = "#{self.class.name}:#{@id}"
|
|
61
|
+
@_id_set = true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
ActiveSupport.run_load_hooks(:all_futures, Base)
|
|
66
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/callbacks"
|
|
4
|
+
|
|
5
|
+
module AllFutures
|
|
6
|
+
module Callbacks
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
CALLBACKS = [:before_save, :after_save, :before_update, :after_update, :before_destroy, :after_destroy]
|
|
10
|
+
|
|
11
|
+
def self.prepended(base)
|
|
12
|
+
base.include(ActiveSupport::Callbacks)
|
|
13
|
+
|
|
14
|
+
base.define_callbacks(:save, :update, :destroy, skip_after_callbacks_if_terminated: true) unless base.respond_to?(:_perform_callbacks) && base._perform_callbacks.present?
|
|
15
|
+
|
|
16
|
+
class << base
|
|
17
|
+
prepend ClassMethods
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def destroy
|
|
22
|
+
perform_operation(:destroy)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def destroy!
|
|
26
|
+
perform_operation(:destroy!)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def save
|
|
30
|
+
perform_operation(:save)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def save!
|
|
34
|
+
perform_operation(:save!)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def update(attrs = {})
|
|
38
|
+
perform_operation(:update, attrs)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def update!(attrs = {})
|
|
42
|
+
perform_operation(:update!, attrs)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def perform_operation(operation, *args)
|
|
46
|
+
if respond_to?(:run_callbacks)
|
|
47
|
+
callback = operation.to_s.sub("!", "").to_sym
|
|
48
|
+
run_callbacks callback do
|
|
49
|
+
method(operation).super_method.call(*args)
|
|
50
|
+
end
|
|
51
|
+
else
|
|
52
|
+
method(operation).super_method.call(*args)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module ClassMethods
|
|
57
|
+
def method_missing(name, *filters, &blk)
|
|
58
|
+
if CALLBACKS.include? name
|
|
59
|
+
callback_context, method_name = name.to_s.split("_")
|
|
60
|
+
set_callback(method_name.to_sym, callback_context.to_sym, *filters, &blk)
|
|
61
|
+
else
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def respond_to_missing?(name, include_all)
|
|
67
|
+
CALLBACKS.include?(name) || super
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
# When true, versioned models save via a Redis Lua compare-and-set so two
|
|
5
|
+
# processes cannot both pass a stale check and silently clobber each other.
|
|
6
|
+
# Default false: the in-process guard rail (GET, check, SET) is enough for
|
|
7
|
+
# most apps and avoids an EVAL on every write.
|
|
8
|
+
mattr_accessor :atomic_locking, instance_accessor: false, default: false
|
|
9
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
module Dirty
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
def attribute_change(attribute)
|
|
8
|
+
_raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
9
|
+
(attribute_was(attribute) == self[attribute]) ? nil : [attribute_was(attribute), self[attribute]]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def attribute_changed?(attribute)
|
|
13
|
+
_raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
14
|
+
changes.key? attribute.to_s
|
|
15
|
+
end
|
|
16
|
+
alias_method :attribute_will_change?, :attribute_changed?
|
|
17
|
+
alias_method :will_save_change_to_attribute?, :attribute_changed?
|
|
18
|
+
|
|
19
|
+
def attribute_changed!(attribute)
|
|
20
|
+
_raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
21
|
+
public_send("#{attribute}_will_change!")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def attribute_previous_change(attribute)
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def attribute_will_change!(attribute)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def clear_attribute_change(attribute)
|
|
33
|
+
_raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def clear_attribute_changes(attr_names = changed)
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def dirty?
|
|
42
|
+
changes.any?
|
|
43
|
+
end
|
|
44
|
+
alias_method :changed_attributes?, :dirty?
|
|
45
|
+
alias_method :has_changes_to_save?, :dirty?
|
|
46
|
+
|
|
47
|
+
def previous_attributes
|
|
48
|
+
attribute_names.each_with_object({}) do |attribute, hash|
|
|
49
|
+
hash[attribute] = attribute_previously_was(attribute)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def mutations_before_last_save
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def restore_attribute(attribute)
|
|
58
|
+
_raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
59
|
+
attribute = attribute.to_s
|
|
60
|
+
if attribute_changed?(attribute)
|
|
61
|
+
self[attribute] = attribute_was(attribute)
|
|
62
|
+
clear_attribute_change(attribute)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def restore_attributes(attr_names = changed)
|
|
67
|
+
attr_names.each { |attribute| _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s) }.each { |attribute| restore_attribute(attribute) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def rollback_attribute(attribute)
|
|
71
|
+
_raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
72
|
+
if self[attribute] != attribute_previously_was(attribute)
|
|
73
|
+
self[attribute] = attribute_previously_was(attribute)
|
|
74
|
+
clear_attribute_change(attribute)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def rollback_attribute!(attribute)
|
|
79
|
+
_raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s)
|
|
80
|
+
# unlike rollback_attribute, keep the mutation tracked so save sees a dirty record
|
|
81
|
+
self[attribute] = attribute_previously_was(attribute) if self[attribute] != attribute_previously_was(attribute)
|
|
82
|
+
save
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def rollback_attributes(attr_names = changed)
|
|
86
|
+
attr_names.each { |attribute| _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s) }.each { |attribute| rollback_attribute(attribute) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def rollback_attributes!(attr_names = changed)
|
|
90
|
+
attr_names.each { |attribute| _raise_unknown_attribute_error(attribute) unless attributes.key?(attribute.to_s) }
|
|
91
|
+
attr_names.each { |attribute| self[attribute] = attribute_previously_was(attribute) if self[attribute] != attribute_previously_was(attribute) }
|
|
92
|
+
save
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def saved_changes?
|
|
96
|
+
saved_changes.any?
|
|
97
|
+
end
|
|
98
|
+
alias_method :previous_changes?, :saved_changes?
|
|
99
|
+
|
|
100
|
+
def saved_changes
|
|
101
|
+
attributes.select { |attribute| attribute_previously_changed? attribute }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
module ClassMethods
|
|
105
|
+
def set_previous_attributes(model, record)
|
|
106
|
+
tracker = ActiveModel::AttributeMutationTracker.new(model.instance_variable_get(:@attributes))
|
|
107
|
+
previous_values = tracker.instance_variable_get(:@attributes).instance_variable_get(:@attributes)
|
|
108
|
+
previous_values.each do |key, attribute|
|
|
109
|
+
original = attribute.instance_variable_get(:@original_attribute)
|
|
110
|
+
original&.instance_variable_set(:@value_before_type_cast, record["previous_attributes"][key])
|
|
111
|
+
end
|
|
112
|
+
model.instance_variable_set :@mutations_before_last_save, tracker
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
module Embeds
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def embedded_in(name, **options)
|
|
9
|
+
autosave = options.delete(:autosave)
|
|
10
|
+
dependent = options.delete(:dependent)
|
|
11
|
+
foreign_key = options.delete(:foreign_key)
|
|
12
|
+
embeds = super
|
|
13
|
+
embeds[name.to_s].options[:autosave] = autosave
|
|
14
|
+
embeds[name.to_s].options[:dependent] = dependent
|
|
15
|
+
# FK lives on the child and points at the parent: government_id, not spy_id
|
|
16
|
+
embeds[name.to_s].options[:foreign_key] = foreign_key || "#{name}_id"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def embeds_one(name, **options)
|
|
20
|
+
autosave = options.delete(:autosave)
|
|
21
|
+
dependent = options.delete(:dependent)
|
|
22
|
+
foreign_key = options.delete(:foreign_key)
|
|
23
|
+
embeds = super
|
|
24
|
+
embeds[name.to_s].options[:autosave] = autosave
|
|
25
|
+
embeds[name.to_s].options[:dependent] = dependent
|
|
26
|
+
embeds[name.to_s].options[:foreign_key] = foreign_key || model_name.singular + "_id"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def embeds_many(name, **options)
|
|
30
|
+
autosave = options.delete(:autosave)
|
|
31
|
+
dependent = options.delete(:dependent)
|
|
32
|
+
foreign_key = options.delete(:foreign_key)
|
|
33
|
+
embeds = super
|
|
34
|
+
embeds[name.to_s].options[:autosave] = autosave
|
|
35
|
+
embeds[name.to_s].options[:dependent] = dependent
|
|
36
|
+
embeds[name.to_s].options[:foreign_key] = foreign_key || model_name.singular + "_id"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
class DeleteRestrictionError < StandardError
|
|
5
|
+
def initialize(name = nil)
|
|
6
|
+
if name
|
|
7
|
+
super("Cannot delete record because of dependent #{name}")
|
|
8
|
+
else
|
|
9
|
+
super("Delete restriction error.")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class InvalidAttribute < StandardError; end
|
|
15
|
+
|
|
16
|
+
class InvalidDependentOption < StandardError; end
|
|
17
|
+
|
|
18
|
+
class MissingForeignKeyError < StandardError; end
|
|
19
|
+
|
|
20
|
+
class ParentModelNotSavedYet < StandardError; end
|
|
21
|
+
|
|
22
|
+
class ReadOnlyRecord < StandardError; end
|
|
23
|
+
|
|
24
|
+
class RecordInvalid < StandardError; end
|
|
25
|
+
|
|
26
|
+
class RecordNotFound < StandardError; end
|
|
27
|
+
|
|
28
|
+
class RecordNotDestroyed < StandardError; end
|
|
29
|
+
|
|
30
|
+
class RecordNotSaved < StandardError; end
|
|
31
|
+
|
|
32
|
+
class RecordStale < StandardError; end
|
|
33
|
+
|
|
34
|
+
class SoleRecordExceeded < StandardError; end
|
|
35
|
+
|
|
36
|
+
class VersionNotFound < StandardError; end
|
|
37
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AllFutures
|
|
4
|
+
module Finder
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
def all
|
|
9
|
+
Kredis.redis.scan_each(match: "#{name}:*").map do |key|
|
|
10
|
+
find(key.delete_prefix("#{name}:"))
|
|
11
|
+
end.sort_by(&:created_at)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def any?(&block)
|
|
15
|
+
block ? all.any?(&block) : all.any?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def count
|
|
19
|
+
Kredis.redis.keys("#{name}:*").size
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def exists?(id)
|
|
23
|
+
return all.any? unless id
|
|
24
|
+
return false if id == false
|
|
25
|
+
[Hash, Array].include?(id.class) ? where(id).any? : Kredis.redis.exists?("#{name}:#{id}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def fifth
|
|
29
|
+
find_nth 4
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fifth!
|
|
33
|
+
fifth || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find(*ids)
|
|
37
|
+
raise AllFutures::RecordNotFound.new("Couldn't find #{name} without an id") unless ids.flatten.present?
|
|
38
|
+
if ids.size == 1 && [String, Integer, Symbol].include?(ids.first.class)
|
|
39
|
+
record = load_model(ids.first)
|
|
40
|
+
model = new record["attributes"].merge(id: ids.first)
|
|
41
|
+
model.changes_applied # loaded values are not user-supplied changes
|
|
42
|
+
load_versions(model, record)
|
|
43
|
+
set_previous_attributes(model, record)
|
|
44
|
+
model.instance_variable_set :@created_at, Time.zone.parse(record["created_at"])
|
|
45
|
+
model.instance_variable_set :@updated_at, Time.zone.parse(record["updated_at"])
|
|
46
|
+
model
|
|
47
|
+
else
|
|
48
|
+
results = ids.flatten.map do |id|
|
|
49
|
+
find(id)
|
|
50
|
+
rescue AllFutures::RecordNotFound
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
return results if results.size == results.compact.size
|
|
54
|
+
raise AllFutures::RecordNotFound.new("Couldn't find all #{name.pluralize} with ids: #{ids.flatten.join(", ")} (found #{results.compact.size} results, but was looking for #{results.size})")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def find_by(attrs = {})
|
|
59
|
+
result = where(attrs)
|
|
60
|
+
result.any? ? result.first : nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def find_by!(attrs = {})
|
|
64
|
+
result = find_by(attrs)
|
|
65
|
+
result.present? ? result : raise(AllFutures::RecordNotFound.new("Couldn't find #{name} with #{_pretty_attrs(attrs)}"))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def find_or_create_by(attrs = {}, &block)
|
|
69
|
+
return create({}, &block) unless attrs.present?
|
|
70
|
+
find_by!(attrs)
|
|
71
|
+
rescue AllFutures::RecordNotFound
|
|
72
|
+
create(attrs, &block)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def find_or_initialize_by(attrs = {}, &block)
|
|
76
|
+
return new({}, &block) unless attrs.present?
|
|
77
|
+
find_by!(attrs)
|
|
78
|
+
rescue AllFutures::RecordNotFound
|
|
79
|
+
new(attrs, &block)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def find_sole_by(attrs = {}, &block)
|
|
83
|
+
found, undesired = where(attrs, &block).first(2)
|
|
84
|
+
|
|
85
|
+
if found.nil?
|
|
86
|
+
raise AllFutures::RecordNotFound.new("Couldn't find #{name} record")
|
|
87
|
+
elsif undesired.present?
|
|
88
|
+
raise AllFutures::SoleRecordExceeded.new("Found multiple #{name} records when expecting only one")
|
|
89
|
+
else
|
|
90
|
+
found
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def first(limit = nil)
|
|
95
|
+
limit ? all.first(limit) : all.first
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def first!
|
|
99
|
+
first || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def forty_two
|
|
103
|
+
find_nth 41
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def forty_two!
|
|
107
|
+
forty_two || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def fourth
|
|
111
|
+
find_nth 3
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def fourth!
|
|
115
|
+
fourth || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def ids
|
|
119
|
+
Kredis.redis.keys("#{name}:*").map { |id| id.delete_prefix("#{name}:") }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def include?(record)
|
|
123
|
+
record.is_a?(self) && exists?(record.id)
|
|
124
|
+
end
|
|
125
|
+
alias_method :member?, :include?
|
|
126
|
+
|
|
127
|
+
def last(limit = nil)
|
|
128
|
+
limit ? all.last(limit) : all.last
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def last!
|
|
132
|
+
last || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def second
|
|
136
|
+
find_nth 1
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def second!
|
|
140
|
+
second || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def second_to_last
|
|
144
|
+
find_nth_from_last 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def second_to_last!
|
|
148
|
+
second_to_last || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def sole
|
|
152
|
+
found, undesired = first(2)
|
|
153
|
+
|
|
154
|
+
if found.nil?
|
|
155
|
+
raise AllFutures::RecordNotFound.new("Couldn't find #{name} record")
|
|
156
|
+
elsif undesired.present?
|
|
157
|
+
raise AllFutures::SoleRecordExceeded.new("Found multiple #{name} records when expecting only one")
|
|
158
|
+
else
|
|
159
|
+
found
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def third
|
|
164
|
+
find_nth 2
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def third!
|
|
168
|
+
third || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def third_to_last
|
|
172
|
+
find_nth_from_last 2
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def third_to_last!
|
|
176
|
+
third_to_last || raise(AllFutures::RecordNotFound.new("Couldn't find #{name} record"))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def where(attrs = {}, &block)
|
|
180
|
+
if block.blank?
|
|
181
|
+
return all if attrs.blank?
|
|
182
|
+
begin
|
|
183
|
+
return [find(attrs.values.first)] if attrs.one? && attrs.keys.first == :id
|
|
184
|
+
rescue AllFutures::RecordNotFound
|
|
185
|
+
return []
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
attrs.each_key { |key| raise AllFutures::InvalidAttribute.new("#{key} is not a valid attribute") unless valid_attribute?(key) }
|
|
190
|
+
all.select do |record|
|
|
191
|
+
attrs.all? do |(attribute, value)|
|
|
192
|
+
record.send(attribute) == value.to_s
|
|
193
|
+
end && (block ? block.call(record) : true)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
def find_nth(index)
|
|
200
|
+
records = all
|
|
201
|
+
(records.size >= index) ? records[index] : nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def find_nth_from_last(index)
|
|
205
|
+
records = all.reverse
|
|
206
|
+
(records.size >= index) ? records[index] : nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def _pretty_attrs(attrs)
|
|
210
|
+
attrs.map { |key, value| "#{key}: #{value}" }.join(", ")
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|