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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11aa0780a8b4e5d41d3010603fff7f4e86f2dd6226e443d1c969d461f115d9a5
4
- data.tar.gz: fa289cbbd0677836e4933cb57392ca7027821e12df1f3eb1cacd8fb87b130ddc
3
+ metadata.gz: 8c3c7d26fc3228203838e82ee2819b5bfe3be9269429e5e4fa0f9fc0f895dca1
4
+ data.tar.gz: 3972d267e94e926212ecfd8dac239daa4b1043cf6a1f567da8c45fcc8ff38403
5
5
  SHA512:
6
- metadata.gz: 8f04eda18abbbf4bf4ab2aae97ff933406dad0692157acb3a3d22b8ca2b4caec99c750489c719f194ddda05e07ee42a310a3bc387ae173364a696809cea87546
7
- data.tar.gz: 48b5015fc97d6137b33ca2b3b92c308f3e9f993d8a6c083bc0255fbac5af6da5c9bba9998801450da6192e2c8b2d4d35c1e67f3ead284d1c8608e745c2be2bc8
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
- error_message = config[:message] || "Cannot delete #{self.class.name} because dependent #{name} exist"
33
- errors.add(:base, error_message)
34
- throw(:abort)
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
- associated_records.each(&:destroy)
33
+ send(name).each(&:destroy)
37
34
  when :delete_all
38
- associated_records.each(&:delete)
35
+ send(name).each(&:delete)
39
36
  when :nullify
40
37
  foreign_key = config[:foreign_key]
41
- associated_records.each { |record| record.update(foreign_key => nil) }
38
+ send(name).each { |record| record.update(foreign_key => nil) }
42
39
  end
43
40
  end
44
41
  end
@@ -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 ActiveSupport::Callbacks
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
- set_callback :create, :before, :generate_primary_key
43
- set_callback :create, :before, :assign_created_timestamp
44
- set_callback :destroy, :before, :check_dependent_associations
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(:@previously_changed, {})
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
- @pending_changes = {}
302
- @previously_changed = {}
289
+ clear_changes_information
303
290
  self
304
291
  end
305
292
 
306
293
  def has_changes_to_save?
307
- changes.any?
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
- next unless respond_to?(setter)
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
- @pending_changes.key?(attr_name.to_s)
441
+ super(attr_name.to_s)
458
442
  end
459
443
 
460
444
  def attribute_was(attr_name)
461
- @pending_changes.dig(attr_name.to_s, 0)
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)
@@ -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
- existing = record.class.where(**conditions)
20
- existing = existing.reject { |r| r.id == record.id } if record.id
21
- existing = existing.select { |r| options[:conditions].call(r) } if options[:conditions]
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 existing.any?
36
+ record.errors.add(attribute, options[:message] || 'has already been taken') if taken
24
37
  end
25
38
  end
26
39
 
27
- # Convenience validation macros (uniqueness, length, numericality, format)
28
- # that extend ActiveModel::Validations for DynamoDB-backed models.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveItem
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.3'
5
5
  end
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.2
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-22 00:00:00.000000000 Z
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: '4.0'
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: '4.0'
83
+ version: '5.0'
84
84
  - !ruby/object:Gem::Dependency
85
85
  name: rake
86
86
  requirement: !ruby/object:Gem::Requirement