activeitem 0.0.1 → 0.0.3
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/CHANGELOG.md +36 -0
- data/LICENSE.txt +1 -1
- data/lib/active_item/associations.rb +21 -24
- data/lib/active_item/base.rb +88 -98
- data/lib/active_item/composed_of.rb +34 -23
- data/lib/active_item/configuration.rb +2 -0
- data/lib/active_item/database_helpers.rb +3 -3
- data/lib/active_item/errors.rb +4 -1
- data/lib/active_item/logging.rb +2 -0
- data/lib/active_item/model_loader.rb +4 -12
- data/lib/active_item/pagination.rb +8 -5
- data/lib/active_item/query_helpers.rb +44 -78
- data/lib/active_item/relation.rb +122 -113
- data/lib/active_item/transaction.rb +3 -1
- data/lib/active_item/validations.rb +22 -70
- data/lib/active_item/version.rb +1 -1
- data/lib/activeitem.rb +3 -0
- metadata +63 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c3c7d26fc3228203838e82ee2819b5bfe3be9269429e5e4fa0f9fc0f895dca1
|
|
4
|
+
data.tar.gz: 3972d267e94e926212ecfd8dac239daa4b1043cf6a1f567da8c45fcc8ff38403
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 02ab351109c3b188a103114e8b051e8f073771aa75769ee315f05f12d4fd0162ec4584113b3e2982f59fdbc6b71bb96a7a8d58ec2e01ae256bb1fc24ee7b014e
|
|
7
|
+
data.tar.gz: d6649e5310e72e084504eca05b61253dde0eb8722149031acd1dfa1d2ba63a9191d70c10cc7f375de06cadfafd991c5b5e1204b6b17dc379159f8bf35bf1c86d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.3
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Replace custom `validates_length_of`, `validates_numericality_of`, `validates_format_of` with ActiveModel built-ins
|
|
8
|
+
- Replace hand-rolled dirty tracking (`@pending_changes`/`@previously_changed`) with `ActiveModel::Dirty`
|
|
9
|
+
- Replace manual `ActiveSupport::Callbacks` DSL with `ActiveModel::Callbacks` + `define_model_callbacks`
|
|
10
|
+
|
|
11
|
+
### Improved
|
|
12
|
+
|
|
13
|
+
- Add `limit` to `UniquenessValidator` queries (limit 2 when excluding self, `.first` for new records)
|
|
14
|
+
- `execute_count_query` now respects `limit_value` and short-circuits pagination
|
|
15
|
+
- `check_dependent_associations` uses `limit(1).any?` for restrict checks
|
|
16
|
+
|
|
17
|
+
## 0.0.2
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- **[Critical]** Pagination cursor validation — decoded JSON is now validated to only contain flat key/value pairs with alphanumeric keys and string/numeric values. Prevents partition traversal via crafted cursors.
|
|
22
|
+
- **[Critical]** Remove arbitrary file require from `model_loader.rb` — `safe_constantize_model` now uses `safe_constantize` with class name format validation instead of requiring files from disk.
|
|
23
|
+
- **[Medium]** Add jitter to exponential backoff in batch operations to prevent thundering herd.
|
|
24
|
+
- **[Low]** Replace `Object.const_get` with `safe_constantize` in `composed_of` to prevent constant hierarchy traversal.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fix `set_created_timestamp` callback not setting `@created_at`, causing DynamoDB `Invalid attribute value type` errors on create
|
|
29
|
+
- Fix duplicate `id=` method definition (Lint/DuplicateMethods) by using `attr_reader :id` with a custom setter
|
|
30
|
+
- Fix duplicate `last` method definition in QueryHelpers
|
|
31
|
+
- Fix duplicate branch in Relation `includes` case statement (Lint/DuplicateBranch)
|
|
32
|
+
- Use `Comparable#clamp` in Pagination and Relation (Style/ComparableClamp)
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Documentation comments for all public modules and classes (Style/Documentation)
|
|
37
|
+
- `--workdir` option to CI DynamoDB service for `act` compatibility
|
|
38
|
+
|
|
3
39
|
## 0.0.1
|
|
4
40
|
|
|
5
41
|
- Initial release
|
data/LICENSE.txt
CHANGED
|
@@ -5,6 +5,9 @@ require 'active_support/inflector'
|
|
|
5
5
|
require_relative 'model_loader'
|
|
6
6
|
|
|
7
7
|
module ActiveItem
|
|
8
|
+
# Provides has_many and belongs_to association macros with lazy loading,
|
|
9
|
+
# preloading support, and dependent record handling (destroy, nullify,
|
|
10
|
+
# restrict).
|
|
8
11
|
module Associations
|
|
9
12
|
extend ActiveSupport::Concern
|
|
10
13
|
include ModelLoader
|
|
@@ -17,25 +20,22 @@ module ActiveItem
|
|
|
17
20
|
self.class._associations.each do |name, config|
|
|
18
21
|
next unless config[:type] == :has_many && config[:dependent]
|
|
19
22
|
|
|
20
|
-
associated_records = send(name)
|
|
21
|
-
has_records = associated_records.respond_to?(:any?) ? associated_records.any? : false
|
|
22
|
-
|
|
23
|
-
next unless has_records
|
|
24
|
-
|
|
25
23
|
case config[:dependent]
|
|
26
24
|
when :restrict_with_exception
|
|
27
|
-
raise DeleteRestrictionError
|
|
25
|
+
raise DeleteRestrictionError, name if send(name).limit(1).any?
|
|
28
26
|
when :restrict_with_error
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
if send(name).limit(1).any?
|
|
28
|
+
error_message = config[:message] || "Cannot delete #{self.class.name} because dependent #{name} exist"
|
|
29
|
+
errors.add(:base, error_message)
|
|
30
|
+
throw(:abort)
|
|
31
|
+
end
|
|
32
32
|
when :destroy
|
|
33
|
-
|
|
33
|
+
send(name).each(&:destroy)
|
|
34
34
|
when :delete_all
|
|
35
|
-
|
|
35
|
+
send(name).each(&:delete)
|
|
36
36
|
when :nullify
|
|
37
37
|
foreign_key = config[:foreign_key]
|
|
38
|
-
|
|
38
|
+
send(name).each { |record| record.update(foreign_key => nil) }
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -87,9 +87,7 @@ module ActiveItem
|
|
|
87
87
|
)
|
|
88
88
|
|
|
89
89
|
foreign_key_sym = foreign_key.to_sym
|
|
90
|
-
unless method_defined?(foreign_key_sym) || private_method_defined?(foreign_key_sym)
|
|
91
|
-
attr_accessor foreign_key_sym
|
|
92
|
-
end
|
|
90
|
+
attr_accessor foreign_key_sym unless method_defined?(foreign_key_sym) || private_method_defined?(foreign_key_sym)
|
|
93
91
|
|
|
94
92
|
validates foreign_key_sym, presence: true unless optional
|
|
95
93
|
|
|
@@ -98,10 +96,10 @@ module ActiveItem
|
|
|
98
96
|
define_method("#{association_name}=") { |record| set_belongs_to_association(association_name, record) }
|
|
99
97
|
|
|
100
98
|
default_foreign_key = "#{association_name}_id"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
return unless foreign_key.to_s != default_foreign_key
|
|
100
|
+
|
|
101
|
+
define_method(default_foreign_key) { send(foreign_key) }
|
|
102
|
+
define_method("#{default_foreign_key}=") { |value| send("#{foreign_key}=", value) }
|
|
105
103
|
end
|
|
106
104
|
end
|
|
107
105
|
|
|
@@ -111,19 +109,17 @@ module ActiveItem
|
|
|
111
109
|
config = self.class._associations[name]
|
|
112
110
|
return Relation.new(Object, conditions: { _empty: true }) unless config
|
|
113
111
|
|
|
114
|
-
if _preloaded_associations.key?(name)
|
|
115
|
-
return Relation.new(nil, preloaded_records: _preloaded_associations[name], class_name: config[:class_name])
|
|
116
|
-
end
|
|
112
|
+
return Relation.new(nil, preloaded_records: _preloaded_associations[name], class_name: config[:class_name]) if _preloaded_associations.key?(name)
|
|
117
113
|
|
|
118
114
|
local_key_value = send(config[:primary_key])
|
|
119
115
|
return Relation.new(nil, conditions: { _empty: true }, class_name: config[:class_name]) if local_key_value.nil?
|
|
120
116
|
|
|
121
117
|
conditions = { config[:foreign_key].to_sym => local_key_value }
|
|
122
118
|
relation = Relation.new(nil, conditions: conditions, index_name: config[:index],
|
|
123
|
-
|
|
119
|
+
class_name: config[:class_name], owner: self)
|
|
124
120
|
|
|
125
121
|
if config[:scope]
|
|
126
|
-
if config[:scope].arity
|
|
122
|
+
if config[:scope].arity.zero?
|
|
127
123
|
relation.instance_exec(&config[:scope]) || relation
|
|
128
124
|
else
|
|
129
125
|
config[:scope].call(relation)
|
|
@@ -149,6 +145,7 @@ module ActiveItem
|
|
|
149
145
|
record = klass.find(foreign_key_value)
|
|
150
146
|
rescue ActiveItem::RecordNotFound
|
|
151
147
|
raise unless config[:optional]
|
|
148
|
+
|
|
152
149
|
record = nil
|
|
153
150
|
end
|
|
154
151
|
instance_variable_set(cache_var, record)
|
data/lib/active_item/base.rb
CHANGED
|
@@ -4,18 +4,22 @@ require 'aws-sdk-dynamodb'
|
|
|
4
4
|
require 'active_support/core_ext/string/inflections'
|
|
5
5
|
require 'active_support/core_ext/hash/indifferent_access'
|
|
6
6
|
require 'active_support/core_ext/array/extract_options'
|
|
7
|
-
require 'active_support/callbacks'
|
|
8
|
-
require 'active_support/concern'
|
|
9
7
|
require 'active_model'
|
|
10
8
|
require 'securerandom'
|
|
11
9
|
|
|
12
10
|
module ActiveItem
|
|
11
|
+
# Base class for all ActiveItem models. Provides persistence, callbacks,
|
|
12
|
+
# validations, dirty tracking, and an ActiveRecord-like interface for
|
|
13
|
+
# DynamoDB tables.
|
|
13
14
|
class Base
|
|
14
15
|
include ActiveModel::Validations
|
|
15
|
-
include
|
|
16
|
+
include ActiveModel::Dirty
|
|
17
|
+
extend ActiveModel::Callbacks
|
|
16
18
|
include Associations
|
|
17
19
|
include Logging
|
|
18
20
|
|
|
21
|
+
define_model_callbacks :save, :create, :update, :destroy, :initialize, :validation
|
|
22
|
+
|
|
19
23
|
def self.const_missing(name)
|
|
20
24
|
ActiveItem.const_defined?(name) ? ActiveItem.const_get(name) : super
|
|
21
25
|
end
|
|
@@ -26,32 +30,30 @@ module ActiveItem
|
|
|
26
30
|
extend QueryHelpers
|
|
27
31
|
extend Validations
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
attr_accessor :id, :created_at, :updated_at, :dbrecord
|
|
33
|
+
attr_reader :id
|
|
34
|
+
attr_accessor :created_at, :updated_at, :dbrecord
|
|
33
35
|
|
|
34
36
|
def id=(value)
|
|
35
37
|
@id = (value.to_s.strip.empty? ? nil : value)
|
|
36
38
|
end
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
before_create :generate_primary_key
|
|
41
|
+
before_create :assign_created_timestamp
|
|
42
|
+
before_destroy :check_dependent_associations
|
|
41
43
|
|
|
42
44
|
def initialize(attributes = {})
|
|
43
|
-
@previously_changed = {}
|
|
44
|
-
@pending_changes = {}
|
|
45
45
|
@_preloaded_counts = {}
|
|
46
46
|
@_preloaded_associations = {}
|
|
47
47
|
@new_record = true
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
return unless attributes.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
attributes.each do |key, value|
|
|
52
|
+
setter = "#{key}="
|
|
53
|
+
send(setter, value) if respond_to?(setter)
|
|
54
54
|
end
|
|
55
|
+
|
|
56
|
+
clear_changes_information
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
def _preloaded_counts
|
|
@@ -63,9 +65,7 @@ module ActiveItem
|
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
def self.attribute_names
|
|
66
|
-
@attribute_names ||=
|
|
67
|
-
instance_methods.grep(/\A[a-z_][a-z0-9_]*=\z/).map { |m| m.to_s.chomp('=') }.sort
|
|
68
|
-
end
|
|
68
|
+
@attribute_names ||= instance_methods.grep(/\A[a-z_][a-z0-9_]*=\z/).map { |m| m.to_s.chomp('=') }.sort
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def populate_attributes_from_item(item)
|
|
@@ -75,11 +75,11 @@ module ActiveItem
|
|
|
75
75
|
value = nil
|
|
76
76
|
found = false
|
|
77
77
|
self.class.dynamo_key_variants(attr_name).each do |key|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
next unless item.key?(key)
|
|
79
|
+
|
|
80
|
+
value = item[key]
|
|
81
|
+
found = true
|
|
82
|
+
break
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
instance_variable_set("@#{attr_name}", value) if found
|
|
@@ -97,19 +97,18 @@ module ActiveItem
|
|
|
97
97
|
attrs.each do |attr|
|
|
98
98
|
attr_name = attr.to_s
|
|
99
99
|
|
|
100
|
+
define_attribute_methods attr_name
|
|
101
|
+
|
|
100
102
|
define_method(attr_name) do
|
|
101
103
|
instance_variable_get("@#{attr_name}")
|
|
102
104
|
end
|
|
103
105
|
|
|
104
106
|
define_method("#{attr_name}=") do |value|
|
|
105
107
|
old_value = instance_variable_get("@#{attr_name}")
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if old_value != value && instance_variable_defined?(:@pending_changes)
|
|
109
|
-
@pending_changes ||= {}
|
|
110
|
-
@pending_changes[attr_name] ||= [old_value, nil]
|
|
111
|
-
@pending_changes[attr_name][1] = value
|
|
108
|
+
if old_value != value
|
|
109
|
+
send("#{attr_name}_will_change!") unless changed_attributes.key?(attr_name)
|
|
112
110
|
end
|
|
111
|
+
instance_variable_set("@#{attr_name}", value)
|
|
113
112
|
end
|
|
114
113
|
end
|
|
115
114
|
end
|
|
@@ -120,12 +119,12 @@ module ActiveItem
|
|
|
120
119
|
|
|
121
120
|
def primary_key=(value)
|
|
122
121
|
remove_method primary_key.to_sym
|
|
123
|
-
remove_method "#{primary_key}="
|
|
122
|
+
remove_method :"#{primary_key}="
|
|
124
123
|
|
|
125
124
|
@primary_key = value.to_s
|
|
126
125
|
|
|
127
126
|
alias_method primary_key.to_sym, :id
|
|
128
|
-
alias_method "#{primary_key}="
|
|
127
|
+
alias_method :"#{primary_key}=", :id=
|
|
129
128
|
end
|
|
130
129
|
|
|
131
130
|
def table_name
|
|
@@ -140,9 +139,7 @@ module ActiveItem
|
|
|
140
139
|
@dynamodb ||= Aws::DynamoDB::Client.new(http_wire_trace: false)
|
|
141
140
|
end
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
@dynamodb = client
|
|
145
|
-
end
|
|
142
|
+
attr_writer :dynamodb
|
|
146
143
|
|
|
147
144
|
def dynamo_attribute_map(mappings = nil)
|
|
148
145
|
if mappings
|
|
@@ -155,6 +152,7 @@ module ActiveItem
|
|
|
155
152
|
def to_dynamo_key(attr_name)
|
|
156
153
|
attr_str = attr_name.to_s
|
|
157
154
|
return dynamo_attribute_map[attr_str] if dynamo_attribute_map.key?(attr_str)
|
|
155
|
+
|
|
158
156
|
attr_str.camelize(:lower)
|
|
159
157
|
end
|
|
160
158
|
|
|
@@ -162,6 +160,7 @@ module ActiveItem
|
|
|
162
160
|
key_str = dynamo_key.to_s
|
|
163
161
|
reverse_map = dynamo_attribute_map.invert
|
|
164
162
|
return reverse_map[key_str] if reverse_map.key?(key_str)
|
|
163
|
+
|
|
165
164
|
key_str.underscore
|
|
166
165
|
end
|
|
167
166
|
|
|
@@ -176,12 +175,12 @@ module ActiveItem
|
|
|
176
175
|
normalized_item = normalize_dynamodb_values(item)
|
|
177
176
|
|
|
178
177
|
record = allocate
|
|
179
|
-
record.instance_variable_set(:@id, normalized_item[
|
|
178
|
+
record.instance_variable_set(:@id, normalized_item[primary_key])
|
|
180
179
|
record.send(:populate_attributes_from_item, normalized_item)
|
|
181
180
|
record.instance_variable_set(:@new_record, false)
|
|
182
|
-
record.instance_variable_set(:@
|
|
183
|
-
record.instance_variable_set(:@pending_changes, {})
|
|
181
|
+
record.instance_variable_set(:@mutations_from_database, nil)
|
|
184
182
|
record.instance_variable_set(:@dbrecord, normalized_item)
|
|
183
|
+
record.send(:clear_changes_information)
|
|
185
184
|
record
|
|
186
185
|
end
|
|
187
186
|
|
|
@@ -208,44 +207,36 @@ module ActiveItem
|
|
|
208
207
|
record
|
|
209
208
|
end
|
|
210
209
|
|
|
211
|
-
# Callback DSL
|
|
212
|
-
def before_save(*args, &
|
|
210
|
+
# Callback DSL — :on option routes before_save/after_save to create/update
|
|
211
|
+
def before_save(*args, &)
|
|
213
212
|
options = args.extract_options!
|
|
214
213
|
if options[:on]
|
|
215
214
|
case options[:on].to_sym
|
|
216
|
-
when :create then set_callback(:create, :before, *args, &
|
|
217
|
-
when :update then set_callback(:update, :before, *args, &
|
|
215
|
+
when :create then set_callback(:create, :before, *args, &)
|
|
216
|
+
when :update then set_callback(:update, :before, *args, &)
|
|
218
217
|
else raise ArgumentError, "Invalid on: option '#{options[:on]}'. Must be :create or :update"
|
|
219
218
|
end
|
|
220
219
|
else
|
|
221
|
-
set_callback(:save, :before, *args, &
|
|
220
|
+
set_callback(:save, :before, *args, &)
|
|
222
221
|
end
|
|
223
222
|
end
|
|
224
223
|
|
|
225
|
-
def after_save(*args, &
|
|
224
|
+
def after_save(*args, &)
|
|
226
225
|
options = args.extract_options!
|
|
227
226
|
if options[:on]
|
|
228
227
|
case options[:on].to_sym
|
|
229
|
-
when :create then set_callback(:create, :after, *args, &
|
|
230
|
-
when :update then set_callback(:update, :after, *args, &
|
|
228
|
+
when :create then set_callback(:create, :after, *args, &)
|
|
229
|
+
when :update then set_callback(:update, :after, *args, &)
|
|
231
230
|
else raise ArgumentError, "Invalid on: option '#{options[:on]}'. Must be :create or :update"
|
|
232
231
|
end
|
|
233
232
|
else
|
|
234
|
-
set_callback(:save, :after, *args, &
|
|
233
|
+
set_callback(:save, :after, *args, &)
|
|
235
234
|
end
|
|
236
235
|
end
|
|
237
236
|
|
|
238
|
-
def before_create(*args, &block) = set_callback(:create, :before, *args, &block)
|
|
239
|
-
def after_create(*args, &block) = set_callback(:create, :after, *args, &block)
|
|
240
|
-
def before_update(*args, &block) = set_callback(:update, :before, *args, &block)
|
|
241
|
-
def after_update(*args, &block) = set_callback(:update, :after, *args, &block)
|
|
242
|
-
def before_validation(*args, &block) = set_callback(:validation, :before, *args, &block)
|
|
243
|
-
def after_validation(*args, &block) = set_callback(:validation, :after, *args, &block)
|
|
244
|
-
def before_destroy(*args, &block) = set_callback(:destroy, :before, *args, &block)
|
|
245
|
-
def after_destroy(*args, &block) = set_callback(:destroy, :after, *args, &block)
|
|
246
|
-
|
|
247
237
|
def scope(name, body)
|
|
248
|
-
raise ArgumentError,
|
|
238
|
+
raise ArgumentError, 'scope body must be callable (Proc/Lambda)' unless body.respond_to?(:call)
|
|
239
|
+
|
|
249
240
|
_scopes[name.to_sym] = body
|
|
250
241
|
define_singleton_method(name) { all.instance_exec(&body) }
|
|
251
242
|
end
|
|
@@ -257,7 +248,8 @@ module ActiveItem
|
|
|
257
248
|
private
|
|
258
249
|
|
|
259
250
|
def default_table_name
|
|
260
|
-
raise
|
|
251
|
+
raise 'Cannot generate table name for anonymous class' unless name
|
|
252
|
+
|
|
261
253
|
ActiveItem.configuration.table_name_for(name)
|
|
262
254
|
end
|
|
263
255
|
|
|
@@ -265,7 +257,7 @@ module ActiveItem
|
|
|
265
257
|
super
|
|
266
258
|
subclass.class_eval do
|
|
267
259
|
alias_method primary_key.to_sym, :id
|
|
268
|
-
alias_method "#{primary_key}="
|
|
260
|
+
alias_method :"#{primary_key}=", :id=
|
|
269
261
|
end
|
|
270
262
|
end
|
|
271
263
|
end
|
|
@@ -279,12 +271,14 @@ module ActiveItem
|
|
|
279
271
|
end
|
|
280
272
|
|
|
281
273
|
def reload
|
|
282
|
-
raise
|
|
274
|
+
raise 'Cannot reload a new record' if new_record?
|
|
275
|
+
|
|
283
276
|
fresh_record = self.class.find(id)
|
|
284
277
|
raise "Record not found: #{self.class.name} with id #{id}" unless fresh_record
|
|
285
278
|
|
|
286
279
|
self.class.attribute_names.each do |attr_name|
|
|
287
280
|
next if attr_name == 'dbrecord'
|
|
281
|
+
|
|
288
282
|
value = fresh_record.instance_variable_get("@#{attr_name}")
|
|
289
283
|
instance_variable_set("@#{attr_name}", value)
|
|
290
284
|
end
|
|
@@ -292,13 +286,12 @@ module ActiveItem
|
|
|
292
286
|
@created_at = fresh_record.created_at
|
|
293
287
|
@updated_at = fresh_record.updated_at
|
|
294
288
|
@dbrecord = fresh_record.dbrecord
|
|
295
|
-
|
|
296
|
-
@previously_changed = {}
|
|
289
|
+
clear_changes_information
|
|
297
290
|
self
|
|
298
291
|
end
|
|
299
292
|
|
|
300
293
|
def has_changes_to_save?
|
|
301
|
-
|
|
294
|
+
changed?
|
|
302
295
|
end
|
|
303
296
|
|
|
304
297
|
def to_h
|
|
@@ -308,12 +301,17 @@ module ActiveItem
|
|
|
308
301
|
def attributes
|
|
309
302
|
attrs = {}
|
|
310
303
|
pk_name = self.class.primary_key
|
|
311
|
-
pk_value =
|
|
304
|
+
pk_value = begin
|
|
305
|
+
send(pk_name)
|
|
306
|
+
rescue StandardError
|
|
307
|
+
instance_variable_get("@#{pk_name}")
|
|
308
|
+
end
|
|
312
309
|
attrs['id'] = pk_value
|
|
313
310
|
attrs[pk_name] = pk_value
|
|
314
311
|
|
|
315
312
|
self.class.attribute_names.each do |attr_name|
|
|
316
313
|
next if attr_name == 'dbrecord'
|
|
314
|
+
|
|
317
315
|
value = instance_variable_get("@#{attr_name}")
|
|
318
316
|
attrs[attr_name] = value unless value.nil?
|
|
319
317
|
end
|
|
@@ -324,11 +322,17 @@ module ActiveItem
|
|
|
324
322
|
end
|
|
325
323
|
|
|
326
324
|
def inspect
|
|
327
|
-
|
|
325
|
+
begin
|
|
326
|
+
send(self.class.primary_key)
|
|
327
|
+
rescue StandardError
|
|
328
|
+
id
|
|
329
|
+
end
|
|
328
330
|
attr_strs = self.class.attribute_names.filter_map do |attr|
|
|
329
331
|
next if attr == 'dbrecord'
|
|
332
|
+
|
|
330
333
|
value = instance_variable_get("@#{attr}")
|
|
331
334
|
next if value.nil?
|
|
335
|
+
|
|
332
336
|
"#{attr}: #{value.inspect}"
|
|
333
337
|
end
|
|
334
338
|
"#<#{self.class.name} #{attr_strs.join(', ')}>"
|
|
@@ -356,9 +360,11 @@ module ActiveItem
|
|
|
356
360
|
end
|
|
357
361
|
|
|
358
362
|
return false if result == false
|
|
363
|
+
|
|
359
364
|
changes_applied
|
|
365
|
+
@new_record = false
|
|
360
366
|
true
|
|
361
|
-
rescue => e
|
|
367
|
+
rescue StandardError => e
|
|
362
368
|
dynamo_logger.error("Failed to save #{self.class.name}: #{e.message}")
|
|
363
369
|
raise e
|
|
364
370
|
end
|
|
@@ -406,10 +412,11 @@ module ActiveItem
|
|
|
406
412
|
def destroy
|
|
407
413
|
result = run_callbacks(:destroy) { perform_destroy }
|
|
408
414
|
return false if result == false
|
|
415
|
+
|
|
409
416
|
true
|
|
410
417
|
rescue DeleteRestrictionError
|
|
411
418
|
false
|
|
412
|
-
rescue => e
|
|
419
|
+
rescue StandardError => e
|
|
413
420
|
dynamo_logger.error("Failed to destroy #{self.class.name}: #{e.message}")
|
|
414
421
|
errors.add(:base, e.message)
|
|
415
422
|
false
|
|
@@ -418,7 +425,7 @@ module ActiveItem
|
|
|
418
425
|
def delete
|
|
419
426
|
perform_destroy
|
|
420
427
|
true
|
|
421
|
-
rescue => e
|
|
428
|
+
rescue StandardError => e
|
|
422
429
|
dynamo_logger.error("Failed to delete #{self.class.name}: #{e.message}")
|
|
423
430
|
false
|
|
424
431
|
end
|
|
@@ -426,38 +433,20 @@ module ActiveItem
|
|
|
426
433
|
def assign_attributes(attributes)
|
|
427
434
|
attributes.each do |key, value|
|
|
428
435
|
setter = "#{key}="
|
|
429
|
-
if respond_to?(setter)
|
|
430
|
-
old_value = send(key)
|
|
431
|
-
@pending_changes[key.to_s] = [old_value, value] if old_value != value
|
|
432
|
-
send(setter, value)
|
|
433
|
-
end
|
|
436
|
+
send(setter, value) if respond_to?(setter)
|
|
434
437
|
end
|
|
435
438
|
end
|
|
436
439
|
|
|
437
440
|
def attribute_changed?(attr_name)
|
|
438
|
-
|
|
441
|
+
super(attr_name.to_s)
|
|
439
442
|
end
|
|
440
443
|
|
|
441
444
|
def attribute_was(attr_name)
|
|
442
|
-
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
def changes
|
|
446
|
-
@pending_changes
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
def previous_changes
|
|
450
|
-
@previously_changed
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
def changes_applied
|
|
454
|
-
@previously_changed = @pending_changes.dup
|
|
455
|
-
@pending_changes = {}
|
|
456
|
-
@new_record = false
|
|
445
|
+
attribute_in_database(attr_name.to_s)
|
|
457
446
|
end
|
|
458
447
|
|
|
459
448
|
def valid?(context = nil)
|
|
460
|
-
return super
|
|
449
|
+
return super if defined?(@running_validations) && @running_validations
|
|
461
450
|
|
|
462
451
|
@running_validations = true
|
|
463
452
|
begin
|
|
@@ -477,8 +466,8 @@ module ActiveItem
|
|
|
477
466
|
instance_variable_set("@#{pk}", @id) if pk != 'id'
|
|
478
467
|
end
|
|
479
468
|
|
|
480
|
-
def
|
|
481
|
-
@created_at ||= Time.now.utc.iso8601
|
|
469
|
+
def assign_created_timestamp
|
|
470
|
+
@created_at ||= Time.now.utc.iso8601 # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
482
471
|
end
|
|
483
472
|
|
|
484
473
|
def dynamodb
|
|
@@ -508,11 +497,11 @@ module ActiveItem
|
|
|
508
497
|
|
|
509
498
|
dynamo_logger.info("#{self.class.name} created (#{self.class.primary_key}: #{id})")
|
|
510
499
|
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
|
511
|
-
errors.add(:id,
|
|
500
|
+
errors.add(:id, 'already exists')
|
|
512
501
|
false
|
|
513
502
|
rescue Aws::DynamoDB::Errors::AccessDeniedException => e
|
|
514
503
|
raise ActiveItem::AccessDeniedError.new(model_name: self.class.name, table: table_name,
|
|
515
|
-
|
|
504
|
+
operation: 'PutItem', original_error: e)
|
|
516
505
|
end
|
|
517
506
|
|
|
518
507
|
def build_dynamodb_item
|
|
@@ -521,6 +510,7 @@ module ActiveItem
|
|
|
521
510
|
dynamodb_attributes.each do |attr|
|
|
522
511
|
value = instance_variable_get("@#{attr}")
|
|
523
512
|
next if value.nil?
|
|
513
|
+
|
|
524
514
|
dynamo_key = self.class.to_dynamo_key(attr)
|
|
525
515
|
item[dynamo_key] = value
|
|
526
516
|
end
|
|
@@ -559,7 +549,7 @@ module ActiveItem
|
|
|
559
549
|
end
|
|
560
550
|
end
|
|
561
551
|
|
|
562
|
-
update_parts <<
|
|
552
|
+
update_parts << 'updatedAt = :updatedAt'
|
|
563
553
|
attr_values[':updatedAt'] = Time.now.utc.iso8601
|
|
564
554
|
|
|
565
555
|
update_expression = "SET #{update_parts.join(', ')}"
|
|
@@ -576,7 +566,7 @@ module ActiveItem
|
|
|
576
566
|
dynamodb.update_item(params)
|
|
577
567
|
rescue Aws::DynamoDB::Errors::AccessDeniedException => e
|
|
578
568
|
raise ActiveItem::AccessDeniedError.new(model_name: self.class.name, table: table_name,
|
|
579
|
-
|
|
569
|
+
operation: 'UpdateItem', original_error: e)
|
|
580
570
|
end
|
|
581
571
|
|
|
582
572
|
def perform_destroy
|
|
@@ -585,7 +575,7 @@ module ActiveItem
|
|
|
585
575
|
dynamo_logger.info("#{self.class.name} deleted (#{key}: #{send(key)})")
|
|
586
576
|
rescue Aws::DynamoDB::Errors::AccessDeniedException => e
|
|
587
577
|
raise ActiveItem::AccessDeniedError.new(model_name: self.class.name, table: table_name,
|
|
588
|
-
|
|
578
|
+
operation: 'DeleteItem', original_error: e)
|
|
589
579
|
end
|
|
590
580
|
end
|
|
591
581
|
end
|