activeitem 0.0.1
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 +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +112 -0
- data/lib/active_item/associations.rb +176 -0
- data/lib/active_item/base.rb +591 -0
- data/lib/active_item/composed_of.rb +195 -0
- data/lib/active_item/configuration.rb +24 -0
- data/lib/active_item/database_helpers.rb +84 -0
- data/lib/active_item/errors.rb +28 -0
- data/lib/active_item/logging.rb +19 -0
- data/lib/active_item/model_loader.rb +23 -0
- data/lib/active_item/pagination.rb +51 -0
- data/lib/active_item/query_helpers.rb +637 -0
- data/lib/active_item/relation.rb +1509 -0
- data/lib/active_item/transaction.rb +97 -0
- data/lib/active_item/validations.rb +95 -0
- data/lib/active_item/version.rb +5 -0
- data/lib/activeitem.rb +31 -0
- metadata +134 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aws-sdk-dynamodb'
|
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
|
5
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
|
6
|
+
require 'active_support/core_ext/array/extract_options'
|
|
7
|
+
require 'active_support/callbacks'
|
|
8
|
+
require 'active_support/concern'
|
|
9
|
+
require 'active_model'
|
|
10
|
+
require 'securerandom'
|
|
11
|
+
|
|
12
|
+
module ActiveItem
|
|
13
|
+
class Base
|
|
14
|
+
include ActiveModel::Validations
|
|
15
|
+
include ActiveSupport::Callbacks
|
|
16
|
+
include Associations
|
|
17
|
+
include Logging
|
|
18
|
+
|
|
19
|
+
def self.const_missing(name)
|
|
20
|
+
ActiveItem.const_defined?(name) ? ActiveItem.const_get(name) : super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
prepend ComposedOf
|
|
24
|
+
|
|
25
|
+
extend DatabaseHelpers
|
|
26
|
+
extend QueryHelpers
|
|
27
|
+
extend Validations
|
|
28
|
+
|
|
29
|
+
define_callbacks :save, :create, :update, :destroy, :validation
|
|
30
|
+
define_model_callbacks :initialize, only: :after
|
|
31
|
+
|
|
32
|
+
attr_accessor :id, :created_at, :updated_at, :dbrecord
|
|
33
|
+
|
|
34
|
+
def id=(value)
|
|
35
|
+
@id = (value.to_s.strip.empty? ? nil : value)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
set_callback :create, :before, :generate_primary_key
|
|
39
|
+
set_callback :create, :before, :set_created_timestamp
|
|
40
|
+
set_callback :destroy, :before, :check_dependent_associations
|
|
41
|
+
|
|
42
|
+
def initialize(attributes = {})
|
|
43
|
+
@previously_changed = {}
|
|
44
|
+
@pending_changes = {}
|
|
45
|
+
@_preloaded_counts = {}
|
|
46
|
+
@_preloaded_associations = {}
|
|
47
|
+
@new_record = true
|
|
48
|
+
|
|
49
|
+
if attributes.is_a?(Hash)
|
|
50
|
+
attributes.each do |key, value|
|
|
51
|
+
setter = "#{key}="
|
|
52
|
+
send(setter, value) if respond_to?(setter)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def _preloaded_counts
|
|
58
|
+
@_preloaded_counts ||= {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def _preloaded_associations
|
|
62
|
+
@_preloaded_associations ||= {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.attribute_names
|
|
66
|
+
@attribute_names ||= begin
|
|
67
|
+
instance_methods.grep(/\A[a-z_][a-z0-9_]*=\z/).map { |m| m.to_s.chomp('=') }.sort
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def populate_attributes_from_item(item)
|
|
72
|
+
self.class.attribute_names.each do |attr_name|
|
|
73
|
+
next if attr_name == 'id'
|
|
74
|
+
|
|
75
|
+
value = nil
|
|
76
|
+
found = false
|
|
77
|
+
self.class.dynamo_key_variants(attr_name).each do |key|
|
|
78
|
+
if item.key?(key)
|
|
79
|
+
value = item[key]
|
|
80
|
+
found = true
|
|
81
|
+
break
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
instance_variable_set("@#{attr_name}", value) if found
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
@created_at = item['createdAt'] || item['created_at']
|
|
89
|
+
@updated_at = item['updatedAt'] || item['updated_at']
|
|
90
|
+
|
|
91
|
+
populate_custom_attributes_from_item(item) if respond_to?(:populate_custom_attributes_from_item, true)
|
|
92
|
+
populate_composed_attributes_from_item(item) if self.class.respond_to?(:compositions) && self.class.compositions.any?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class << self
|
|
96
|
+
def attr_accessor(*attrs)
|
|
97
|
+
attrs.each do |attr|
|
|
98
|
+
attr_name = attr.to_s
|
|
99
|
+
|
|
100
|
+
define_method(attr_name) do
|
|
101
|
+
instance_variable_get("@#{attr_name}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
define_method("#{attr_name}=") do |value|
|
|
105
|
+
old_value = instance_variable_get("@#{attr_name}")
|
|
106
|
+
instance_variable_set("@#{attr_name}", value)
|
|
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
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def primary_key
|
|
118
|
+
@primary_key ||= 'id'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def primary_key=(value)
|
|
122
|
+
remove_method primary_key.to_sym
|
|
123
|
+
remove_method "#{primary_key}=".to_sym
|
|
124
|
+
|
|
125
|
+
@primary_key = value.to_s
|
|
126
|
+
|
|
127
|
+
alias_method primary_key.to_sym, :id
|
|
128
|
+
alias_method "#{primary_key}=".to_sym, :id=
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def table_name
|
|
132
|
+
@table_name || default_table_name
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def table_name=(value)
|
|
136
|
+
@table_name = value.to_s
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def dynamodb
|
|
140
|
+
@dynamodb ||= Aws::DynamoDB::Client.new(http_wire_trace: false)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def dynamodb=(client)
|
|
144
|
+
@dynamodb = client
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def dynamo_attribute_map(mappings = nil)
|
|
148
|
+
if mappings
|
|
149
|
+
@dynamo_attribute_map = mappings.transform_keys(&:to_s)
|
|
150
|
+
else
|
|
151
|
+
@dynamo_attribute_map || {}
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def to_dynamo_key(attr_name)
|
|
156
|
+
attr_str = attr_name.to_s
|
|
157
|
+
return dynamo_attribute_map[attr_str] if dynamo_attribute_map.key?(attr_str)
|
|
158
|
+
attr_str.camelize(:lower)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def from_dynamo_key(dynamo_key)
|
|
162
|
+
key_str = dynamo_key.to_s
|
|
163
|
+
reverse_map = dynamo_attribute_map.invert
|
|
164
|
+
return reverse_map[key_str] if reverse_map.key?(key_str)
|
|
165
|
+
key_str.underscore
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def dynamo_key_variants(attr_name)
|
|
169
|
+
attr_str = attr_name.to_s
|
|
170
|
+
primary_key = to_dynamo_key(attr_str)
|
|
171
|
+
camel_case = attr_str.camelize(:lower)
|
|
172
|
+
[primary_key, camel_case, attr_str].uniq
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def instantiate(item)
|
|
176
|
+
normalized_item = normalize_dynamodb_values(item)
|
|
177
|
+
|
|
178
|
+
record = allocate
|
|
179
|
+
record.instance_variable_set(:@id, normalized_item[self.primary_key])
|
|
180
|
+
record.send(:populate_attributes_from_item, normalized_item)
|
|
181
|
+
record.instance_variable_set(:@new_record, false)
|
|
182
|
+
record.instance_variable_set(:@previously_changed, {})
|
|
183
|
+
record.instance_variable_set(:@pending_changes, {})
|
|
184
|
+
record.instance_variable_set(:@dbrecord, normalized_item)
|
|
185
|
+
record
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def normalize_dynamodb_values(obj)
|
|
189
|
+
case obj
|
|
190
|
+
when BigDecimal
|
|
191
|
+
obj.frac.zero? ? obj.to_i : obj.to_f
|
|
192
|
+
when Hash
|
|
193
|
+
obj.transform_values { |v| normalize_dynamodb_values(v) }
|
|
194
|
+
when Array
|
|
195
|
+
obj.map { |v| normalize_dynamodb_values(v) }
|
|
196
|
+
else
|
|
197
|
+
obj
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def find_or_create_by(attributes, &block)
|
|
202
|
+
record = find_by(**attributes)
|
|
203
|
+
return record if record
|
|
204
|
+
|
|
205
|
+
record = new(**attributes)
|
|
206
|
+
block.call(record) if block_given?
|
|
207
|
+
record.save
|
|
208
|
+
record
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Callback DSL
|
|
212
|
+
def before_save(*args, &block)
|
|
213
|
+
options = args.extract_options!
|
|
214
|
+
if options[:on]
|
|
215
|
+
case options[:on].to_sym
|
|
216
|
+
when :create then set_callback(:create, :before, *args, &block)
|
|
217
|
+
when :update then set_callback(:update, :before, *args, &block)
|
|
218
|
+
else raise ArgumentError, "Invalid on: option '#{options[:on]}'. Must be :create or :update"
|
|
219
|
+
end
|
|
220
|
+
else
|
|
221
|
+
set_callback(:save, :before, *args, &block)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def after_save(*args, &block)
|
|
226
|
+
options = args.extract_options!
|
|
227
|
+
if options[:on]
|
|
228
|
+
case options[:on].to_sym
|
|
229
|
+
when :create then set_callback(:create, :after, *args, &block)
|
|
230
|
+
when :update then set_callback(:update, :after, *args, &block)
|
|
231
|
+
else raise ArgumentError, "Invalid on: option '#{options[:on]}'. Must be :create or :update"
|
|
232
|
+
end
|
|
233
|
+
else
|
|
234
|
+
set_callback(:save, :after, *args, &block)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
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
|
+
def scope(name, body)
|
|
248
|
+
raise ArgumentError, "scope body must be callable (Proc/Lambda)" unless body.respond_to?(:call)
|
|
249
|
+
_scopes[name.to_sym] = body
|
|
250
|
+
define_singleton_method(name) { all.instance_exec(&body) }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def _scopes
|
|
254
|
+
@_scopes ||= {}
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
private
|
|
258
|
+
|
|
259
|
+
def default_table_name
|
|
260
|
+
raise "Cannot generate table name for anonymous class" unless name
|
|
261
|
+
ActiveItem.configuration.table_name_for(name)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def inherited(subclass)
|
|
265
|
+
super
|
|
266
|
+
subclass.class_eval do
|
|
267
|
+
alias_method primary_key.to_sym, :id
|
|
268
|
+
alias_method "#{primary_key}=".to_sym, :id=
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def new_record?
|
|
274
|
+
@new_record != false
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def persisted?
|
|
278
|
+
!new_record?
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def reload
|
|
282
|
+
raise "Cannot reload a new record" if new_record?
|
|
283
|
+
fresh_record = self.class.find(id)
|
|
284
|
+
raise "Record not found: #{self.class.name} with id #{id}" unless fresh_record
|
|
285
|
+
|
|
286
|
+
self.class.attribute_names.each do |attr_name|
|
|
287
|
+
next if attr_name == 'dbrecord'
|
|
288
|
+
value = fresh_record.instance_variable_get("@#{attr_name}")
|
|
289
|
+
instance_variable_set("@#{attr_name}", value)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
@created_at = fresh_record.created_at
|
|
293
|
+
@updated_at = fresh_record.updated_at
|
|
294
|
+
@dbrecord = fresh_record.dbrecord
|
|
295
|
+
@pending_changes = {}
|
|
296
|
+
@previously_changed = {}
|
|
297
|
+
self
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def has_changes_to_save?
|
|
301
|
+
changes.any?
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def to_h
|
|
305
|
+
attributes.with_indifferent_access
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def attributes
|
|
309
|
+
attrs = {}
|
|
310
|
+
pk_name = self.class.primary_key
|
|
311
|
+
pk_value = send(pk_name) rescue instance_variable_get("@#{pk_name}")
|
|
312
|
+
attrs['id'] = pk_value
|
|
313
|
+
attrs[pk_name] = pk_value
|
|
314
|
+
|
|
315
|
+
self.class.attribute_names.each do |attr_name|
|
|
316
|
+
next if attr_name == 'dbrecord'
|
|
317
|
+
value = instance_variable_get("@#{attr_name}")
|
|
318
|
+
attrs[attr_name] = value unless value.nil?
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
attrs['created_at'] = @created_at
|
|
322
|
+
attrs['updated_at'] = @updated_at
|
|
323
|
+
attrs
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def inspect
|
|
327
|
+
pk_value = send(self.class.primary_key) rescue id
|
|
328
|
+
attr_strs = self.class.attribute_names.filter_map do |attr|
|
|
329
|
+
next if attr == 'dbrecord'
|
|
330
|
+
value = instance_variable_get("@#{attr}")
|
|
331
|
+
next if value.nil?
|
|
332
|
+
"#{attr}: #{value.inspect}"
|
|
333
|
+
end
|
|
334
|
+
"#<#{self.class.name} #{attr_strs.join(', ')}>"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def update(attributes)
|
|
338
|
+
assign_attributes(attributes)
|
|
339
|
+
save
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def update!(attributes)
|
|
343
|
+
assign_attributes(attributes)
|
|
344
|
+
save!
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def save(validate: true)
|
|
348
|
+
return false if validate && !run_validations
|
|
349
|
+
|
|
350
|
+
result = run_callbacks :save do
|
|
351
|
+
if new_record?
|
|
352
|
+
run_callbacks(:create) { perform_create }
|
|
353
|
+
else
|
|
354
|
+
run_callbacks(:update) { perform_update }
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
return false if result == false
|
|
359
|
+
changes_applied
|
|
360
|
+
true
|
|
361
|
+
rescue => e
|
|
362
|
+
dynamo_logger.error("Failed to save #{self.class.name}: #{e.message}")
|
|
363
|
+
raise e
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def save!
|
|
367
|
+
raise StandardError, "Validation failed: #{errors.full_messages.join(', ')}" unless save
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def self.create(attributes = {})
|
|
371
|
+
obj = new(attributes)
|
|
372
|
+
obj.save
|
|
373
|
+
obj
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def self.create!(attributes = {})
|
|
377
|
+
obj = new(attributes)
|
|
378
|
+
obj.save!
|
|
379
|
+
obj
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def self.transaction
|
|
383
|
+
txn = Transaction.new
|
|
384
|
+
yield txn
|
|
385
|
+
txn.execute!
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def self.transaction_find(items)
|
|
389
|
+
return [] if items.empty?
|
|
390
|
+
raise TransactionError, "DynamoDB transactions are limited to 100 items (got #{items.length})" if items.length > 100
|
|
391
|
+
|
|
392
|
+
transact_items = items.map do |item|
|
|
393
|
+
{ get: { table_name: item[:model].table_name, key: { item[:model].primary_key.to_s => item[:key] } } }
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
client = items.first[:model].dynamodb
|
|
397
|
+
response = client.transact_get_items(transact_items: transact_items)
|
|
398
|
+
|
|
399
|
+
response.responses.each_with_index.map do |resp, idx|
|
|
400
|
+
items[idx][:model].instantiate(resp.item) if resp.item
|
|
401
|
+
end
|
|
402
|
+
rescue Aws::DynamoDB::Errors::TransactionCanceledException => e
|
|
403
|
+
raise TransactionError, "Transaction read cancelled: #{e.message}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def destroy
|
|
407
|
+
result = run_callbacks(:destroy) { perform_destroy }
|
|
408
|
+
return false if result == false
|
|
409
|
+
true
|
|
410
|
+
rescue DeleteRestrictionError
|
|
411
|
+
false
|
|
412
|
+
rescue => e
|
|
413
|
+
dynamo_logger.error("Failed to destroy #{self.class.name}: #{e.message}")
|
|
414
|
+
errors.add(:base, e.message)
|
|
415
|
+
false
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def delete
|
|
419
|
+
perform_destroy
|
|
420
|
+
true
|
|
421
|
+
rescue => e
|
|
422
|
+
dynamo_logger.error("Failed to delete #{self.class.name}: #{e.message}")
|
|
423
|
+
false
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def assign_attributes(attributes)
|
|
427
|
+
attributes.each do |key, value|
|
|
428
|
+
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
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def attribute_changed?(attr_name)
|
|
438
|
+
@pending_changes.key?(attr_name.to_s)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def attribute_was(attr_name)
|
|
442
|
+
@pending_changes.dig(attr_name.to_s, 0)
|
|
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
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def valid?(context = nil)
|
|
460
|
+
return super(context) if defined?(@running_validations) && @running_validations
|
|
461
|
+
|
|
462
|
+
@running_validations = true
|
|
463
|
+
begin
|
|
464
|
+
run_callbacks(:validation) { super(context) }
|
|
465
|
+
ensure
|
|
466
|
+
@running_validations = false
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
private
|
|
471
|
+
|
|
472
|
+
def generate_primary_key
|
|
473
|
+
@id = nil if @id.to_s.strip.empty?
|
|
474
|
+
@id ||= SecureRandom.uuid
|
|
475
|
+
|
|
476
|
+
pk = self.class.primary_key
|
|
477
|
+
instance_variable_set("@#{pk}", @id) if pk != 'id'
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def set_created_timestamp
|
|
481
|
+
@created_at ||= Time.now.utc.iso8601
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def dynamodb
|
|
485
|
+
self.class.dynamodb
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def table_name
|
|
489
|
+
self.class.table_name
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def run_validations
|
|
493
|
+
context = new_record? ? :create : :update
|
|
494
|
+
valid?(context)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def perform_create
|
|
498
|
+
item = build_dynamodb_item
|
|
499
|
+
item['createdAt'] = @created_at
|
|
500
|
+
item['updatedAt'] = Time.now.utc.iso8601
|
|
501
|
+
|
|
502
|
+
dynamodb.put_item(
|
|
503
|
+
table_name: table_name,
|
|
504
|
+
item: item,
|
|
505
|
+
condition_expression: 'attribute_not_exists(#pk)',
|
|
506
|
+
expression_attribute_names: { '#pk' => self.class.primary_key.to_s }
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
dynamo_logger.info("#{self.class.name} created (#{self.class.primary_key}: #{id})")
|
|
510
|
+
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException
|
|
511
|
+
errors.add(:id, "already exists")
|
|
512
|
+
false
|
|
513
|
+
rescue Aws::DynamoDB::Errors::AccessDeniedException => e
|
|
514
|
+
raise ActiveItem::AccessDeniedError.new(model_name: self.class.name, table: table_name,
|
|
515
|
+
operation: 'PutItem', original_error: e)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def build_dynamodb_item
|
|
519
|
+
item = { self.class.primary_key.to_s => id }
|
|
520
|
+
|
|
521
|
+
dynamodb_attributes.each do |attr|
|
|
522
|
+
value = instance_variable_get("@#{attr}")
|
|
523
|
+
next if value.nil?
|
|
524
|
+
dynamo_key = self.class.to_dynamo_key(attr)
|
|
525
|
+
item[dynamo_key] = value
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
item
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def dynamodb_attributes
|
|
532
|
+
attrs = self.class.attribute_names - [self.class.primary_key.sub('_id', ''), 'id', 'dbrecord']
|
|
533
|
+
|
|
534
|
+
if self.class.respond_to?(:compositions) && self.class.compositions.any?
|
|
535
|
+
composed_attrs = self.class.compositions.values.flat_map { |c| c[:mapping].keys.map(&:to_s) }
|
|
536
|
+
attrs -= composed_attrs
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
attrs
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def perform_update
|
|
543
|
+
return if changes.empty?
|
|
544
|
+
|
|
545
|
+
update_parts = []
|
|
546
|
+
remove_parts = []
|
|
547
|
+
attr_values = {}
|
|
548
|
+
attr_names = {}
|
|
549
|
+
|
|
550
|
+
changes.each_with_index do |(field, (_old_val, new_val)), idx|
|
|
551
|
+
dynamo_key = self.class.to_dynamo_key(field)
|
|
552
|
+
if new_val.nil?
|
|
553
|
+
remove_parts << "#field#{idx}"
|
|
554
|
+
attr_names["#field#{idx}"] = dynamo_key
|
|
555
|
+
else
|
|
556
|
+
update_parts << "#field#{idx} = :val#{idx}"
|
|
557
|
+
attr_names["#field#{idx}"] = dynamo_key
|
|
558
|
+
attr_values[":val#{idx}"] = new_val
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
update_parts << "updatedAt = :updatedAt"
|
|
563
|
+
attr_values[':updatedAt'] = Time.now.utc.iso8601
|
|
564
|
+
|
|
565
|
+
update_expression = "SET #{update_parts.join(', ')}"
|
|
566
|
+
update_expression += " REMOVE #{remove_parts.join(', ')}" if remove_parts.any?
|
|
567
|
+
|
|
568
|
+
params = {
|
|
569
|
+
table_name: table_name,
|
|
570
|
+
key: { self.class.primary_key.to_s => id },
|
|
571
|
+
update_expression: update_expression,
|
|
572
|
+
expression_attribute_values: attr_values
|
|
573
|
+
}
|
|
574
|
+
params[:expression_attribute_names] = attr_names if attr_names.any?
|
|
575
|
+
|
|
576
|
+
dynamodb.update_item(params)
|
|
577
|
+
rescue Aws::DynamoDB::Errors::AccessDeniedException => e
|
|
578
|
+
raise ActiveItem::AccessDeniedError.new(model_name: self.class.name, table: table_name,
|
|
579
|
+
operation: 'UpdateItem', original_error: e)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def perform_destroy
|
|
583
|
+
key = self.class.primary_key.to_s
|
|
584
|
+
dynamodb.delete_item(table_name: table_name, key: { key => send(key) })
|
|
585
|
+
dynamo_logger.info("#{self.class.name} deleted (#{key}: #{send(key)})")
|
|
586
|
+
rescue Aws::DynamoDB::Errors::AccessDeniedException => e
|
|
587
|
+
raise ActiveItem::AccessDeniedError.new(model_name: self.class.name, table: table_name,
|
|
588
|
+
operation: 'DeleteItem', original_error: e)
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
end
|