activeitem 0.0.2 → 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 +14 -0
- data/lib/active_item/associations.rb +9 -12
- data/lib/active_item/base.rb +23 -53
- data/lib/active_item/relation.rb +3 -1
- data/lib/active_item/validations.rb +19 -59
- data/lib/active_item/version.rb +1 -1
- metadata +4 -4
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,19 @@
|
|
|
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
|
+
|
|
3
17
|
## 0.0.2
|
|
4
18
|
|
|
5
19
|
### Security
|
|
@@ -20,25 +20,22 @@ module ActiveItem
|
|
|
20
20
|
self.class._associations.each do |name, config|
|
|
21
21
|
next unless config[:type] == :has_many && config[:dependent]
|
|
22
22
|
|
|
23
|
-
associated_records = send(name)
|
|
24
|
-
has_records = associated_records.respond_to?(:any?) ? associated_records.any? : false
|
|
25
|
-
|
|
26
|
-
next unless has_records
|
|
27
|
-
|
|
28
23
|
case config[:dependent]
|
|
29
24
|
when :restrict_with_exception
|
|
30
|
-
raise DeleteRestrictionError, name
|
|
25
|
+
raise DeleteRestrictionError, name if send(name).limit(1).any?
|
|
31
26
|
when :restrict_with_error
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
35
32
|
when :destroy
|
|
36
|
-
|
|
33
|
+
send(name).each(&:destroy)
|
|
37
34
|
when :delete_all
|
|
38
|
-
|
|
35
|
+
send(name).each(&:delete)
|
|
39
36
|
when :nullify
|
|
40
37
|
foreign_key = config[:foreign_key]
|
|
41
|
-
|
|
38
|
+
send(name).each { |record| record.update(foreign_key => nil) }
|
|
42
39
|
end
|
|
43
40
|
end
|
|
44
41
|
end
|
data/lib/active_item/base.rb
CHANGED
|
@@ -4,8 +4,6 @@ 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
|
|
|
@@ -15,10 +13,13 @@ module ActiveItem
|
|
|
15
13
|
# DynamoDB tables.
|
|
16
14
|
class Base
|
|
17
15
|
include ActiveModel::Validations
|
|
18
|
-
include
|
|
16
|
+
include ActiveModel::Dirty
|
|
17
|
+
extend ActiveModel::Callbacks
|
|
19
18
|
include Associations
|
|
20
19
|
include Logging
|
|
21
20
|
|
|
21
|
+
define_model_callbacks :save, :create, :update, :destroy, :initialize, :validation
|
|
22
|
+
|
|
22
23
|
def self.const_missing(name)
|
|
23
24
|
ActiveItem.const_defined?(name) ? ActiveItem.const_get(name) : super
|
|
24
25
|
end
|
|
@@ -29,9 +30,6 @@ module ActiveItem
|
|
|
29
30
|
extend QueryHelpers
|
|
30
31
|
extend Validations
|
|
31
32
|
|
|
32
|
-
define_callbacks :save, :create, :update, :destroy, :validation
|
|
33
|
-
define_model_callbacks :initialize, only: :after
|
|
34
|
-
|
|
35
33
|
attr_reader :id
|
|
36
34
|
attr_accessor :created_at, :updated_at, :dbrecord
|
|
37
35
|
|
|
@@ -39,13 +37,11 @@ module ActiveItem
|
|
|
39
37
|
@id = (value.to_s.strip.empty? ? nil : value)
|
|
40
38
|
end
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
before_create :generate_primary_key
|
|
41
|
+
before_create :assign_created_timestamp
|
|
42
|
+
before_destroy :check_dependent_associations
|
|
45
43
|
|
|
46
44
|
def initialize(attributes = {})
|
|
47
|
-
@previously_changed = {}
|
|
48
|
-
@pending_changes = {}
|
|
49
45
|
@_preloaded_counts = {}
|
|
50
46
|
@_preloaded_associations = {}
|
|
51
47
|
@new_record = true
|
|
@@ -56,6 +52,8 @@ module ActiveItem
|
|
|
56
52
|
setter = "#{key}="
|
|
57
53
|
send(setter, value) if respond_to?(setter)
|
|
58
54
|
end
|
|
55
|
+
|
|
56
|
+
clear_changes_information
|
|
59
57
|
end
|
|
60
58
|
|
|
61
59
|
def _preloaded_counts
|
|
@@ -99,19 +97,18 @@ module ActiveItem
|
|
|
99
97
|
attrs.each do |attr|
|
|
100
98
|
attr_name = attr.to_s
|
|
101
99
|
|
|
100
|
+
define_attribute_methods attr_name
|
|
101
|
+
|
|
102
102
|
define_method(attr_name) do
|
|
103
103
|
instance_variable_get("@#{attr_name}")
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
define_method("#{attr_name}=") do |value|
|
|
107
107
|
old_value = instance_variable_get("@#{attr_name}")
|
|
108
|
+
if old_value != value
|
|
109
|
+
send("#{attr_name}_will_change!") unless changed_attributes.key?(attr_name)
|
|
110
|
+
end
|
|
108
111
|
instance_variable_set("@#{attr_name}", value)
|
|
109
|
-
|
|
110
|
-
return unless old_value != value && instance_variable_defined?(:@pending_changes)
|
|
111
|
-
|
|
112
|
-
@pending_changes ||= {}
|
|
113
|
-
@pending_changes[attr_name] ||= [old_value, nil]
|
|
114
|
-
@pending_changes[attr_name][1] = value
|
|
115
112
|
end
|
|
116
113
|
end
|
|
117
114
|
end
|
|
@@ -181,9 +178,9 @@ module ActiveItem
|
|
|
181
178
|
record.instance_variable_set(:@id, normalized_item[primary_key])
|
|
182
179
|
record.send(:populate_attributes_from_item, normalized_item)
|
|
183
180
|
record.instance_variable_set(:@new_record, false)
|
|
184
|
-
record.instance_variable_set(:@
|
|
185
|
-
record.instance_variable_set(:@pending_changes, {})
|
|
181
|
+
record.instance_variable_set(:@mutations_from_database, nil)
|
|
186
182
|
record.instance_variable_set(:@dbrecord, normalized_item)
|
|
183
|
+
record.send(:clear_changes_information)
|
|
187
184
|
record
|
|
188
185
|
end
|
|
189
186
|
|
|
@@ -210,7 +207,7 @@ module ActiveItem
|
|
|
210
207
|
record
|
|
211
208
|
end
|
|
212
209
|
|
|
213
|
-
# Callback DSL
|
|
210
|
+
# Callback DSL — :on option routes before_save/after_save to create/update
|
|
214
211
|
def before_save(*args, &)
|
|
215
212
|
options = args.extract_options!
|
|
216
213
|
if options[:on]
|
|
@@ -237,15 +234,6 @@ module ActiveItem
|
|
|
237
234
|
end
|
|
238
235
|
end
|
|
239
236
|
|
|
240
|
-
def before_create(...) = set_callback(:create, :before, ...)
|
|
241
|
-
def after_create(...) = set_callback(:create, :after, ...)
|
|
242
|
-
def before_update(...) = set_callback(:update, :before, ...)
|
|
243
|
-
def after_update(...) = set_callback(:update, :after, ...)
|
|
244
|
-
def before_validation(...) = set_callback(:validation, :before, ...)
|
|
245
|
-
def after_validation(...) = set_callback(:validation, :after, ...)
|
|
246
|
-
def before_destroy(...) = set_callback(:destroy, :before, ...)
|
|
247
|
-
def after_destroy(...) = set_callback(:destroy, :after, ...)
|
|
248
|
-
|
|
249
237
|
def scope(name, body)
|
|
250
238
|
raise ArgumentError, 'scope body must be callable (Proc/Lambda)' unless body.respond_to?(:call)
|
|
251
239
|
|
|
@@ -298,13 +286,12 @@ module ActiveItem
|
|
|
298
286
|
@created_at = fresh_record.created_at
|
|
299
287
|
@updated_at = fresh_record.updated_at
|
|
300
288
|
@dbrecord = fresh_record.dbrecord
|
|
301
|
-
|
|
302
|
-
@previously_changed = {}
|
|
289
|
+
clear_changes_information
|
|
303
290
|
self
|
|
304
291
|
end
|
|
305
292
|
|
|
306
293
|
def has_changes_to_save?
|
|
307
|
-
|
|
294
|
+
changed?
|
|
308
295
|
end
|
|
309
296
|
|
|
310
297
|
def to_h
|
|
@@ -375,6 +362,7 @@ module ActiveItem
|
|
|
375
362
|
return false if result == false
|
|
376
363
|
|
|
377
364
|
changes_applied
|
|
365
|
+
@new_record = false
|
|
378
366
|
true
|
|
379
367
|
rescue StandardError => e
|
|
380
368
|
dynamo_logger.error("Failed to save #{self.class.name}: #{e.message}")
|
|
@@ -445,34 +433,16 @@ module ActiveItem
|
|
|
445
433
|
def assign_attributes(attributes)
|
|
446
434
|
attributes.each do |key, value|
|
|
447
435
|
setter = "#{key}="
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
old_value = send(key)
|
|
451
|
-
@pending_changes[key.to_s] = [old_value, value] if old_value != value
|
|
452
|
-
send(setter, value)
|
|
436
|
+
send(setter, value) if respond_to?(setter)
|
|
453
437
|
end
|
|
454
438
|
end
|
|
455
439
|
|
|
456
440
|
def attribute_changed?(attr_name)
|
|
457
|
-
|
|
441
|
+
super(attr_name.to_s)
|
|
458
442
|
end
|
|
459
443
|
|
|
460
444
|
def attribute_was(attr_name)
|
|
461
|
-
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
def changes
|
|
465
|
-
@pending_changes
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
def previous_changes
|
|
469
|
-
@previously_changed
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
def changes_applied
|
|
473
|
-
@previously_changed = @pending_changes.dup
|
|
474
|
-
@pending_changes = {}
|
|
475
|
-
@new_record = false
|
|
445
|
+
attribute_in_database(attr_name.to_s)
|
|
476
446
|
end
|
|
477
447
|
|
|
478
448
|
def valid?(context = nil)
|
data/lib/active_item/relation.rb
CHANGED
|
@@ -765,6 +765,7 @@ module ActiveItem
|
|
|
765
765
|
build_count_scan_params(normalized_conditions)
|
|
766
766
|
end
|
|
767
767
|
params[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
|
768
|
+
params[:limit] = limit_value if limit_value
|
|
768
769
|
|
|
769
770
|
response = if effective_index && normalized_conditions.any?
|
|
770
771
|
resolved_model.dynamodb.query(params)
|
|
@@ -775,9 +776,10 @@ module ActiveItem
|
|
|
775
776
|
total += response.count
|
|
776
777
|
exclusive_start_key = response.last_evaluated_key
|
|
777
778
|
break unless exclusive_start_key
|
|
779
|
+
break if limit_value && total >= limit_value
|
|
778
780
|
end
|
|
779
781
|
|
|
780
|
-
total
|
|
782
|
+
limit_value ? [total, limit_value].min : total
|
|
781
783
|
rescue Aws::DynamoDB::Errors::AccessDeniedException => e
|
|
782
784
|
raise ActiveItem::AccessDeniedError.new(model_name: resolved_model.name, table: resolved_model.table_name,
|
|
783
785
|
operation: 'Count', original_error: e)
|
|
@@ -16,72 +16,32 @@ module ActiveItem
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
relation = record.class.where(**conditions)
|
|
20
|
+
|
|
21
|
+
if options[:conditions]
|
|
22
|
+
# Custom conditions require loading records for Ruby-side filtering
|
|
23
|
+
existing = relation.to_a
|
|
24
|
+
existing = existing.reject { |r| r.id == record.id } if record.id
|
|
25
|
+
existing = existing.select { |r| options[:conditions].call(r) }
|
|
26
|
+
taken = existing.any?
|
|
27
|
+
elsif record.id
|
|
28
|
+
# Need to exclude current record — fetch up to 2 (current + one other)
|
|
29
|
+
existing = relation.limit(2).to_a
|
|
30
|
+
taken = existing.any? { |r| r.id != record.id }
|
|
31
|
+
else
|
|
32
|
+
# New record — just check if any match exists
|
|
33
|
+
taken = !relation.first.nil?
|
|
34
|
+
end
|
|
22
35
|
|
|
23
|
-
record.errors.add(attribute, options[:message] || 'has already been taken') if
|
|
36
|
+
record.errors.add(attribute, options[:message] || 'has already been taken') if taken
|
|
24
37
|
end
|
|
25
38
|
end
|
|
26
39
|
|
|
27
|
-
# Convenience validation
|
|
28
|
-
#
|
|
40
|
+
# Convenience validation macro for uniqueness (DynamoDB-specific).
|
|
41
|
+
# Length, numericality, and format validations are provided by ActiveModel.
|
|
29
42
|
module Validations
|
|
30
43
|
def validates_uniqueness_of(*attributes, **options)
|
|
31
44
|
validates(*attributes, uniqueness: options.empty? || options)
|
|
32
45
|
end
|
|
33
|
-
|
|
34
|
-
def validates_length_of(*attributes, **options)
|
|
35
|
-
attributes.each do |attribute|
|
|
36
|
-
validate do
|
|
37
|
-
value = send(attribute)
|
|
38
|
-
next if value.nil?
|
|
39
|
-
|
|
40
|
-
length = value.to_s.length
|
|
41
|
-
|
|
42
|
-
errors.add(attribute, options[:message] || "is too short (minimum is #{options[:minimum]} characters)") if options[:minimum] && length < options[:minimum]
|
|
43
|
-
errors.add(attribute, options[:message] || "is too long (maximum is #{options[:maximum]} characters)") if options[:maximum] && length > options[:maximum]
|
|
44
|
-
if options[:in] && !options[:in].include?(length)
|
|
45
|
-
errors.add(attribute, options[:message] || "length must be between #{options[:in].min} and #{options[:in].max} characters")
|
|
46
|
-
end
|
|
47
|
-
errors.add(attribute, options[:message] || "must be exactly #{options[:is]} characters") if options[:is] && length != options[:is]
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def validates_numericality_of(*attributes, **options)
|
|
53
|
-
attributes.each do |attribute|
|
|
54
|
-
validate do
|
|
55
|
-
value = send(attribute)
|
|
56
|
-
next if value.nil?
|
|
57
|
-
|
|
58
|
-
unless value.is_a?(Numeric) || value.to_s.match?(/\A-?\d+(\.\d+)?\z/)
|
|
59
|
-
errors.add(attribute, options[:message] || 'is not a number')
|
|
60
|
-
next
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
num_value = value.to_f
|
|
64
|
-
|
|
65
|
-
if options[:only_integer] && num_value != num_value.to_i
|
|
66
|
-
errors.add(attribute, options[:message] || 'must be an integer')
|
|
67
|
-
next
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
errors.add(attribute, options[:message] || "must be greater than #{options[:greater_than]}") if options[:greater_than] && num_value <= options[:greater_than]
|
|
71
|
-
if options[:greater_than_or_equal_to] && num_value < options[:greater_than_or_equal_to]
|
|
72
|
-
errors.add(attribute, options[:message] || "must be greater than or equal to #{options[:greater_than_or_equal_to]}")
|
|
73
|
-
end
|
|
74
|
-
errors.add(attribute, options[:message] || "must be less than #{options[:less_than]}") if options[:less_than] && num_value >= options[:less_than]
|
|
75
|
-
if options[:less_than_or_equal_to] && num_value > options[:less_than_or_equal_to]
|
|
76
|
-
errors.add(attribute, options[:message] || "must be less than or equal to #{options[:less_than_or_equal_to]}")
|
|
77
|
-
end
|
|
78
|
-
errors.add(attribute, options[:message] || "must be equal to #{options[:equal_to]}") if options[:equal_to] && num_value != options[:equal_to]
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def validates_format_of(*attributes, **options)
|
|
84
|
-
attributes.each { |attribute| validates attribute, format: options }
|
|
85
|
-
end
|
|
86
46
|
end
|
|
87
47
|
end
|
data/lib/active_item/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeitem
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-05-
|
|
12
|
+
date: 2026-05-23 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activemodel
|
|
@@ -73,14 +73,14 @@ dependencies:
|
|
|
73
73
|
requirements:
|
|
74
74
|
- - "~>"
|
|
75
75
|
- !ruby/object:Gem::Version
|
|
76
|
-
version: '
|
|
76
|
+
version: '5.0'
|
|
77
77
|
type: :development
|
|
78
78
|
prerelease: false
|
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
|
80
80
|
requirements:
|
|
81
81
|
- - "~>"
|
|
82
82
|
- !ruby/object:Gem::Version
|
|
83
|
-
version: '
|
|
83
|
+
version: '5.0'
|
|
84
84
|
- !ruby/object:Gem::Dependency
|
|
85
85
|
name: rake
|
|
86
86
|
requirement: !ruby/object:Gem::Requirement
|