Class: ActiveItem::Base
Overview
Base class for all ActiveItem models. Provides persistence, callbacks, validations, dirty tracking, and an ActiveRecord-like interface for DynamoDB tables.
Constant Summary
QueryHelpers::RECENT_INDEX
Class Attribute Summary collapse
Instance Attribute Summary collapse
Class Method Summary
collapse
Instance Method Summary
collapse
exists?, get, put, query, scan
all, all_records, batch_find, batch_write, count, delete_all, exists?, find, find_by, first, includes, indexes, last, none, recent, where
validates_uniqueness_of
#check_dependent_associations
#safe_constantize_model
Methods included from ComposedOf
#populate_composed_attributes_from_item
Constructor Details
#initialize(attributes = {}) ⇒ Base
Returns a new instance of Base.
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
# File 'lib/active_item/base.rb', line 44
def initialize(attributes = {})
@_preloaded_counts = {}
@_preloaded_associations = {}
@new_record = true
return unless attributes.is_a?(Hash)
attributes.each do |key, value|
setter = "#{key}="
send(setter, value) if respond_to?(setter)
end
clear_changes_information
end
|
Class Attribute Details
.dynamodb ⇒ Object
138
139
140
|
# File 'lib/active_item/base.rb', line 138
def dynamodb
@dynamodb ||= Aws::DynamoDB::Client.new(http_wire_trace: false)
end
|
Instance Attribute Details
#created_at ⇒ Object
Returns the value of attribute created_at.
34
35
36
|
# File 'lib/active_item/base.rb', line 34
def created_at
@created_at
end
|
#dbrecord ⇒ Object
Returns the value of attribute dbrecord.
34
35
36
|
# File 'lib/active_item/base.rb', line 34
def dbrecord
@dbrecord
end
|
#id ⇒ Object
Returns the value of attribute id.
33
34
35
|
# File 'lib/active_item/base.rb', line 33
def id
@id
end
|
#updated_at ⇒ Object
Returns the value of attribute updated_at.
34
35
36
|
# File 'lib/active_item/base.rb', line 34
def updated_at
@updated_at
end
|
Class Method Details
._scopes ⇒ Object
244
245
246
|
# File 'lib/active_item/base.rb', line 244
def _scopes
@_scopes ||= {}
end
|
.after_save(*args) ⇒ Object
224
225
226
227
228
229
230
231
232
233
234
235
|
# File 'lib/active_item/base.rb', line 224
def after_save(*args, &)
options = args.
if options[:on]
case options[:on].to_sym
when :create then set_callback(:create, :after, *args, &)
when :update then set_callback(:update, :after, *args, &)
else raise ArgumentError, "Invalid on: option '#{options[:on]}'. Must be :create or :update"
end
else
set_callback(:save, :after, *args, &)
end
end
|
.attr_accessor(*attrs) ⇒ Object
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
# File 'lib/active_item/base.rb', line 96
def attr_accessor(*attrs)
attrs.each do |attr|
attr_name = attr.to_s
define_attribute_methods attr_name
define_method(attr_name) do
instance_variable_get("@#{attr_name}")
end
define_method("#{attr_name}=") do |value|
old_value = instance_variable_get("@#{attr_name}")
if old_value != value
send("#{attr_name}_will_change!") unless changed_attributes.key?(attr_name)
end
instance_variable_set("@#{attr_name}", value)
end
end
end
|
.attribute_names ⇒ Object
67
68
69
|
# File 'lib/active_item/base.rb', line 67
def self.attribute_names
@attribute_names ||= instance_methods.grep(/\A[a-z_][a-z0-9_]*=\z/).map { |m| m.to_s.chomp('=') }.sort
end
|
.before_save(*args) ⇒ Object
Callback DSL — :on option routes before_save/after_save to create/update
211
212
213
214
215
216
217
218
219
220
221
222
|
# File 'lib/active_item/base.rb', line 211
def before_save(*args, &)
options = args.
if options[:on]
case options[:on].to_sym
when :create then set_callback(:create, :before, *args, &)
when :update then set_callback(:update, :before, *args, &)
else raise ArgumentError, "Invalid on: option '#{options[:on]}'. Must be :create or :update"
end
else
set_callback(:save, :before, *args, &)
end
end
|
.const_missing(name) ⇒ Object
23
24
25
|
# File 'lib/active_item/base.rb', line 23
def self.const_missing(name)
ActiveItem.const_defined?(name) ? ActiveItem.const_get(name) : super
end
|
.create(attributes = {}) ⇒ Object
376
377
378
379
380
|
# File 'lib/active_item/base.rb', line 376
def self.create(attributes = {})
obj = new(attributes)
obj.save
obj
end
|
.create!(attributes = {}) ⇒ Object
382
383
384
385
386
|
# File 'lib/active_item/base.rb', line 382
def self.create!(attributes = {})
obj = new(attributes)
obj.save!
obj
end
|
.dynamo_attribute_map(mappings = nil) ⇒ Object
144
145
146
147
148
149
150
|
# File 'lib/active_item/base.rb', line 144
def dynamo_attribute_map(mappings = nil)
if mappings
@dynamo_attribute_map = mappings.transform_keys(&:to_s)
else
@dynamo_attribute_map || {}
end
end
|
.dynamo_key_variants(attr_name) ⇒ Object
167
168
169
170
171
172
|
# File 'lib/active_item/base.rb', line 167
def dynamo_key_variants(attr_name)
attr_str = attr_name.to_s
primary_key = to_dynamo_key(attr_str)
camel_case = attr_str.camelize(:lower)
[primary_key, camel_case, attr_str].uniq
end
|
.find_or_create_by(attributes, &block) ⇒ Object
200
201
202
203
204
205
206
207
208
|
# File 'lib/active_item/base.rb', line 200
def find_or_create_by(attributes, &block)
record = find_by(**attributes)
return record if record
record = new(**attributes)
block.call(record) if block_given?
record.save
record
end
|
.from_dynamo_key(dynamo_key) ⇒ Object
159
160
161
162
163
164
165
|
# File 'lib/active_item/base.rb', line 159
def from_dynamo_key(dynamo_key)
key_str = dynamo_key.to_s
reverse_map = dynamo_attribute_map.invert
return reverse_map[key_str] if reverse_map.key?(key_str)
key_str.underscore
end
|
.instantiate(item) ⇒ Object
174
175
176
177
178
179
180
181
182
183
184
185
|
# File 'lib/active_item/base.rb', line 174
def instantiate(item)
normalized_item = normalize_dynamodb_values(item)
record = allocate
record.instance_variable_set(:@id, normalized_item[primary_key])
record.send(:populate_attributes_from_item, normalized_item)
record.instance_variable_set(:@new_record, false)
record.instance_variable_set(:@mutations_from_database, nil)
record.instance_variable_set(:@dbrecord, normalized_item)
record.send(:clear_changes_information)
record
end
|
.normalize_dynamodb_values(obj) ⇒ Object
187
188
189
190
191
192
193
194
195
196
197
198
|
# File 'lib/active_item/base.rb', line 187
def normalize_dynamodb_values(obj)
case obj
when BigDecimal
obj.frac.zero? ? obj.to_i : obj.to_f
when Hash
obj.transform_values { |v| normalize_dynamodb_values(v) }
when Array
obj.map { |v| normalize_dynamodb_values(v) }
else
obj
end
end
|
.primary_key ⇒ Object
116
117
118
|
# File 'lib/active_item/base.rb', line 116
def primary_key
@primary_key ||= 'id'
end
|
.primary_key=(value) ⇒ Object
120
121
122
123
124
125
126
127
128
|
# File 'lib/active_item/base.rb', line 120
def primary_key=(value)
remove_method primary_key.to_sym
remove_method :"#{primary_key}="
@primary_key = value.to_s
alias_method primary_key.to_sym, :id
alias_method :"#{primary_key}=", :id=
end
|
.scope(name, body) ⇒ Object
237
238
239
240
241
242
|
# File 'lib/active_item/base.rb', line 237
def scope(name, body)
raise ArgumentError, 'scope body must be callable (Proc/Lambda)' unless body.respond_to?(:call)
_scopes[name.to_sym] = body
define_singleton_method(name) { all.instance_exec(&body) }
end
|
.table_name ⇒ Object
130
131
132
|
# File 'lib/active_item/base.rb', line 130
def table_name
@table_name || default_table_name
end
|
.table_name=(value) ⇒ Object
134
135
136
|
# File 'lib/active_item/base.rb', line 134
def table_name=(value)
@table_name = value.to_s
end
|
.to_dynamo_key(attr_name) ⇒ Object
152
153
154
155
156
157
|
# File 'lib/active_item/base.rb', line 152
def to_dynamo_key(attr_name)
attr_str = attr_name.to_s
return dynamo_attribute_map[attr_str] if dynamo_attribute_map.key?(attr_str)
attr_str.camelize(:lower)
end
|
.transaction {|txn| ... } ⇒ Object
388
389
390
391
392
|
# File 'lib/active_item/base.rb', line 388
def self.transaction
txn = Transaction.new
yield txn
txn.execute!
end
|
.transaction_find(items) ⇒ Object
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
|
# File 'lib/active_item/base.rb', line 394
def self.transaction_find(items)
return [] if items.empty?
raise TransactionError, "DynamoDB transactions are limited to 100 items (got #{items.length})" if items.length > 100
transact_items = items.map do |item|
{ get: { table_name: item[:model].table_name, key: { item[:model].primary_key.to_s => item[:key] } } }
end
client = items.first[:model].dynamodb
response = client.transact_get_items(transact_items: transact_items)
response.responses.each_with_index.map do |resp, idx|
items[idx][:model].instantiate(resp.item) if resp.item
end
rescue Aws::DynamoDB::Errors::TransactionCanceledException => e
raise TransactionError, "Transaction read cancelled: #{e.message}"
end
|
Instance Method Details
#_preloaded_associations ⇒ Object
63
64
65
|
# File 'lib/active_item/base.rb', line 63
def _preloaded_associations
@_preloaded_associations ||= {}
end
|
#_preloaded_counts ⇒ Object
59
60
61
|
# File 'lib/active_item/base.rb', line 59
def _preloaded_counts
@_preloaded_counts ||= {}
end
|
#assign_attributes(attributes) ⇒ Object
433
434
435
436
437
438
|
# File 'lib/active_item/base.rb', line 433
def assign_attributes(attributes)
attributes.each do |key, value|
setter = "#{key}="
send(setter, value) if respond_to?(setter)
end
end
|
#attribute_changed?(attr_name) ⇒ Boolean
440
441
442
|
# File 'lib/active_item/base.rb', line 440
def attribute_changed?(attr_name)
super(attr_name.to_s)
end
|
#attribute_was(attr_name) ⇒ Object
444
445
446
|
# File 'lib/active_item/base.rb', line 444
def attribute_was(attr_name)
changed_attributes[attr_name.to_s]
end
|
#attributes ⇒ Object
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
|
# File 'lib/active_item/base.rb', line 301
def attributes
attrs = {}
pk_name = self.class.primary_key
pk_value = begin
send(pk_name)
rescue StandardError
instance_variable_get("@#{pk_name}")
end
attrs['id'] = pk_value
attrs[pk_name] = pk_value
self.class.attribute_names.each do |attr_name|
next if attr_name == 'dbrecord'
value = instance_variable_get("@#{attr_name}")
attrs[attr_name] = value unless value.nil?
end
attrs['created_at'] = @created_at
attrs['updated_at'] = @updated_at
attrs
end
|
#delete ⇒ Object
425
426
427
428
429
430
431
|
# File 'lib/active_item/base.rb', line 425
def delete
perform_destroy
true
rescue StandardError => e
dynamo_logger.error("Failed to delete #{self.class.name}: #{e.message}")
false
end
|
#destroy ⇒ Object
412
413
414
415
416
417
418
419
420
421
422
423
|
# File 'lib/active_item/base.rb', line 412
def destroy
result = run_callbacks(:destroy) { perform_destroy }
return false if result == false
true
rescue DeleteRestrictionError
false
rescue StandardError => e
dynamo_logger.error("Failed to destroy #{self.class.name}: #{e.message}")
errors.add(:base, e.message)
false
end
|
#has_changes_to_save? ⇒ Boolean
293
294
295
|
# File 'lib/active_item/base.rb', line 293
def has_changes_to_save?
changed?
end
|
#inspect ⇒ Object
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
|
# File 'lib/active_item/base.rb', line 324
def inspect
begin
send(self.class.primary_key)
rescue StandardError
id
end
attr_strs = self.class.attribute_names.filter_map do |attr|
next if attr == 'dbrecord'
value = instance_variable_get("@#{attr}")
next if value.nil?
"#{attr}: #{value.inspect}"
end
"#<#{self.class.name} #{attr_strs.join(', ')}>"
end
|
#new_record? ⇒ Boolean
265
266
267
|
# File 'lib/active_item/base.rb', line 265
def new_record?
@new_record != false
end
|
#persisted? ⇒ Boolean
269
270
271
|
# File 'lib/active_item/base.rb', line 269
def persisted?
!new_record?
end
|
#populate_attributes_from_item(item) ⇒ Object
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
# File 'lib/active_item/base.rb', line 71
def populate_attributes_from_item(item)
self.class.attribute_names.each do |attr_name|
next if attr_name == 'id'
value = nil
found = false
self.class.dynamo_key_variants(attr_name).each do |key|
next unless item.key?(key)
value = item[key]
found = true
break
end
instance_variable_set("@#{attr_name}", value) if found
end
@created_at = item['createdAt'] || item['created_at']
@updated_at = item['updatedAt'] || item['updated_at']
populate_custom_attributes_from_item(item) if respond_to?(:populate_custom_attributes_from_item, true)
populate_composed_attributes_from_item(item) if self.class.respond_to?(:compositions) && self.class.compositions.any?
end
|
#reload ⇒ Object
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
|
# File 'lib/active_item/base.rb', line 273
def reload
raise 'Cannot reload a new record' if new_record?
fresh_record = self.class.find(id)
raise "Record not found: #{self.class.name} with id #{id}" unless fresh_record
self.class.attribute_names.each do |attr_name|
next if attr_name == 'dbrecord'
value = fresh_record.instance_variable_get("@#{attr_name}")
instance_variable_set("@#{attr_name}", value)
end
@created_at = fresh_record.created_at
@updated_at = fresh_record.updated_at
@dbrecord = fresh_record.dbrecord
clear_changes_information
self
end
|
#save(validate: true) ⇒ Object
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
|
# File 'lib/active_item/base.rb', line 351
def save(validate: true)
return false if validate && !run_validations
result = run_callbacks :save do
if new_record?
run_callbacks(:create) { perform_create }
else
run_callbacks(:update) { perform_update }
end
end
return false if result == false
changes_applied
@new_record = false
true
rescue StandardError => e
dynamo_logger.error("Failed to save #{self.class.name}: #{e.message}")
raise e
end
|
#save! ⇒ Object
372
373
374
|
# File 'lib/active_item/base.rb', line 372
def save!
raise StandardError, "Validation failed: #{errors.full_messages.join(', ')}" unless save
end
|
#to_h ⇒ Object
297
298
299
|
# File 'lib/active_item/base.rb', line 297
def to_h
attributes.with_indifferent_access
end
|
#update(attributes) ⇒ Object
341
342
343
344
|
# File 'lib/active_item/base.rb', line 341
def update(attributes)
assign_attributes(attributes)
save
end
|
#update!(attributes) ⇒ Object
346
347
348
349
|
# File 'lib/active_item/base.rb', line 346
def update!(attributes)
assign_attributes(attributes)
save!
end
|
#valid?(context = nil) ⇒ Boolean
448
449
450
451
452
453
454
455
456
457
|
# File 'lib/active_item/base.rb', line 448
def valid?(context = nil)
return super if defined?(@running_validations) && @running_validations
@running_validations = true
begin
run_callbacks(:validation) { super(context) }
ensure
@running_validations = false
end
end
|