activeitem 0.0.20 → 0.0.21

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: 652850a906bc798042da7b5c3ea1396aeef9775057ad1199030354cff2b24b73
4
- data.tar.gz: 55001585d52f922c371cd6ca6cc61beef3f6f5ea98e4ca268d0bfe1e6ebf8d5a
3
+ metadata.gz: 976cb7f41429d4ca2489b0a67a87f39ca3720bc223b02ad9c1d4e2e244f6a700
4
+ data.tar.gz: e5c0fd6be8a2ea1ccfc00793942d91973e53b9d434fec9b67e92b8da9660049d
5
5
  SHA512:
6
- metadata.gz: 64fb13d70a43f1f6a5668c3e93b6e1f24ceec853ea6f7d9e974a7fd5b53c7a2f8b66e9d582d6f12d10498a892a10895712092b1bbcac4929c6cad86fc164d770
7
- data.tar.gz: e6495e6752c6b8ffc0b8ef98451141ad8816fe069e4e33a40c7b65f45b4bf02981390bfab503149fce243e354d9842e1ce02bfb9f75f270af40dc5593d51ab5a
6
+ metadata.gz: de5d8d0d4f7b6a986369e0604eec719dd686537009967b00302543abc507db28298b65222cb85e5fb22f9ee7e86568ec4934dc2f761956b6f880d89a74ad5456
7
+ data.tar.gz: 160ca85fad18fcd70b5650f274077c2a8fc841f6943b6d601a1effc13408c74ce037593c25dc6a58b4aa0a04f465b1616159719416d710e77fd10e1a2a134737
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.21
4
+
5
+ ### Added
6
+
7
+ - **GSI key type validation** — ActiveItem now validates that Global Secondary Index (GSI) key attributes have valid DynamoDB types at write time. Valid types are: String (non-empty), Numeric (Integer, Float, BigDecimal), and Binary (StringIO). Invalid types (Array, Hash, Boolean, Symbol, Date, Time, empty strings) now raise `ActiveItem::InvalidGsiKeyTypeError` immediately, with a helpful error message identifying the attribute and index. This catches common bugs like passing `Date.today` instead of `date.to_s`, or accidentally setting an array where a string was expected.
8
+
9
+ ```ruby
10
+ # Raises InvalidGsiKeyTypeError: Item#status has invalid type Array for GSI 'StatusIndex'
11
+ item = Item.new(status: ['a', 'b', 'c'])
12
+ item.save
13
+
14
+ # Raises InvalidGsiKeyTypeError for empty strings
15
+ item = Item.new(status: '')
16
+ item.save
17
+ ```
18
+
3
19
  ## 0.0.19
4
20
 
5
21
  ### Added
@@ -752,6 +752,8 @@ module ActiveItem
752
752
  item[dynamo_key] = collection.map(&:to_embedded_hash)
753
753
  end
754
754
 
755
+ validate_gsi_key_types!(item)
756
+
755
757
  item
756
758
  end
757
759
 
@@ -776,6 +778,9 @@ module ActiveItem
776
778
  embedded_changed = embedded_associations_changed?
777
779
  return if changes.empty? && !embedded_changed
778
780
 
781
+ # Validate GSI key types for changed attributes before building the update
782
+ validate_gsi_key_types_for_changes!
783
+
779
784
  run_embedded_callbacks(:update) if embedded_changed
780
785
  run_embedded_callbacks(:save) if embedded_changed
781
786
 
@@ -854,5 +859,107 @@ module ActiveItem
854
859
  raise ActiveItem::AccessDeniedError.new(model_name: self.class.name, table: table_name,
855
860
  operation: 'DeleteItem', original_error: e)
856
861
  end
862
+
863
+ # Validates that all GSI key attributes have valid types for DynamoDB.
864
+ # GSI keys can only be String, Number (Integer/Float/BigDecimal), or Binary (StringIO).
865
+ #
866
+ # @param item [Hash] The DynamoDB item being built
867
+ # @raise [InvalidGsiKeyTypeError] if any GSI key has an invalid type
868
+ def validate_gsi_key_types!(item)
869
+ return unless self.class.respond_to?(:indexes)
870
+
871
+ indexes = self.class.indexes
872
+ return if indexes.nil? || indexes.empty?
873
+
874
+ indexes.each do |index_name, config|
875
+ # Check partition key
876
+ partition_key = config[:partition_key]&.to_s
877
+ if partition_key && item.key?(partition_key)
878
+ value = item[partition_key]
879
+ validate_gsi_key_value!(partition_key, value, index_name) unless value.nil?
880
+ end
881
+
882
+ # Check sort key if present
883
+ sort_key = config[:sort_key]&.to_s
884
+ if sort_key && item.key?(sort_key)
885
+ value = item[sort_key]
886
+ validate_gsi_key_value!(sort_key, value, index_name) unless value.nil?
887
+ end
888
+ end
889
+ end
890
+
891
+ # Validates GSI key types for changed attributes during updates.
892
+ # Only validates the attributes that are being changed, not the entire item.
893
+ #
894
+ # @raise [InvalidGsiKeyTypeError] if any changed GSI key has an invalid type
895
+ def validate_gsi_key_types_for_changes!
896
+ return unless self.class.respond_to?(:indexes)
897
+
898
+ indexes = self.class.indexes
899
+ return if indexes.nil? || indexes.empty?
900
+
901
+ # Build a hash of changed dynamo keys and their new values
902
+ changed_dynamo_keys = {}
903
+ changes.each do |field, (_old_val, new_val)|
904
+ next if new_val.nil? # nil values are being removed, no type validation needed
905
+
906
+ dynamo_key = self.class.to_dynamo_key(field)
907
+ changed_dynamo_keys[dynamo_key] = new_val
908
+ end
909
+
910
+ return if changed_dynamo_keys.empty?
911
+
912
+ indexes.each do |index_name, config|
913
+ # Check partition key if it's being changed
914
+ partition_key = config[:partition_key]&.to_s
915
+ if partition_key && changed_dynamo_keys.key?(partition_key)
916
+ validate_gsi_key_value!(partition_key, changed_dynamo_keys[partition_key], index_name)
917
+ end
918
+
919
+ # Check sort key if it's being changed
920
+ sort_key = config[:sort_key]&.to_s
921
+ if sort_key && changed_dynamo_keys.key?(sort_key)
922
+ validate_gsi_key_value!(sort_key, changed_dynamo_keys[sort_key], index_name)
923
+ end
924
+ end
925
+ end
926
+
927
+ # Validates that a single GSI key value has a valid type.
928
+ # DynamoDB GSI keys support: String, Number (Integer/Float/BigDecimal), Binary (StringIO).
929
+ # Empty strings are not allowed for GSI keys.
930
+ #
931
+ # @param attribute [String] The attribute name
932
+ # @param value [Object] The attribute value
933
+ # @param index_name [String] The GSI name (for error messages)
934
+ # @raise [InvalidGsiKeyTypeError] if the value has an invalid type
935
+ def validate_gsi_key_value!(attribute, value, index_name)
936
+ return if valid_gsi_key_type?(value)
937
+
938
+ raise InvalidGsiKeyTypeError.new(
939
+ model_name: self.class.name,
940
+ attribute: attribute,
941
+ index_name: index_name,
942
+ value: value
943
+ )
944
+ end
945
+
946
+ # Checks if a value is a valid type for a DynamoDB GSI key.
947
+ # Valid types: String (non-empty), Numeric (Integer, Float, BigDecimal), Binary (StringIO).
948
+ # Empty strings are explicitly rejected as DynamoDB does not allow them for key attributes.
949
+ #
950
+ # @param value [Object] The value to check
951
+ # @return [Boolean] true if the value is a valid GSI key type
952
+ def valid_gsi_key_type?(value)
953
+ case value
954
+ when String
955
+ !value.empty? # Empty strings are not allowed for GSI keys
956
+ when Integer, Float, BigDecimal
957
+ true
958
+ when StringIO
959
+ true
960
+ else
961
+ false
962
+ end
963
+ end
857
964
  end
858
965
  end
@@ -82,4 +82,20 @@ module ActiveItem
82
82
  super(msg)
83
83
  end
84
84
  end
85
+
86
+ # Raised when a GSI key attribute has an invalid type.
87
+ # DynamoDB GSI keys must be String, Number (Integer/Float/BigDecimal), or Binary (StringIO).
88
+ class InvalidGsiKeyTypeError < StandardError
89
+ attr_reader :model_name, :attribute, :index_name, :value, :value_type
90
+
91
+ def initialize(model_name:, attribute:, index_name:, value:)
92
+ @model_name = model_name
93
+ @attribute = attribute
94
+ @index_name = index_name
95
+ @value = value
96
+ @value_type = value.class.name
97
+ super("#{model_name}##{attribute} has invalid type #{@value_type} for GSI '#{index_name}'. " \
98
+ 'GSI keys must be String, Numeric (Integer/Float/BigDecimal), or Binary (StringIO).')
99
+ end
100
+ end
85
101
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveItem
4
- VERSION = '0.0.20'
4
+ VERSION = '0.0.21'
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.20
4
+ version: 0.0.21
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-08-05 00:00:00.000000000 Z
12
+ date: 2026-08-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activemodel