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 +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/active_item/base.rb +107 -0
- data/lib/active_item/errors.rb +16 -0
- data/lib/active_item/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 976cb7f41429d4ca2489b0a67a87f39ca3720bc223b02ad9c1d4e2e244f6a700
|
|
4
|
+
data.tar.gz: e5c0fd6be8a2ea1ccfc00793942d91973e53b9d434fec9b67e92b8da9660049d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/active_item/base.rb
CHANGED
|
@@ -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
|
data/lib/active_item/errors.rb
CHANGED
|
@@ -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
|
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.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-
|
|
12
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activemodel
|