activeitem 0.0.18 → 0.0.19
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 +24 -0
- data/lib/active_item/associations.rb +85 -2
- data/lib/active_item/relation.rb +5 -0
- data/lib/active_item/validations.rb +42 -0
- data/lib/active_item/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7992e08eadef33c715ba67e3869454bca7cf174cae56e419982b81de608075c6
|
|
4
|
+
data.tar.gz: dc1e6e803c91bb774f7e0c2edf43f6ca744ac10221bba26b8a5a4a4d7466f425
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee3f192c4bc8d0ea583f85462cb28841092e0389ed7682b6cfec753f446b687174385f74b81fa58ddc411c34b7dea1fd348aeea243a1109b0ae1ae0915565036
|
|
7
|
+
data.tar.gz: f21580968c7db755fbc2c75e4cdb2cf8c9b3ab7f400a5a83954fb297215a78ec9adf4968f2ed3e526d771747c80460d50a8b7c23f70c6e2489a5d8e84f03b4a2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`validates_associated`** — Validates that associated records (loaded via `has_many`) are valid before saving the parent. Only checks in-memory/loaded records — does not trigger DB queries for unloaded associations.
|
|
8
|
+
|
|
9
|
+
- **`accepts_nested_attributes_for`** — Define nested attribute writers for has_many associations. Supports create, update, and destroy of child records through the parent's save. Nested saves are now wrapped in a DynamoDB transaction for atomicity — if any child operation fails, none are committed.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class Conversation < ActiveItem::Base
|
|
13
|
+
has_many :messages
|
|
14
|
+
accepts_nested_attributes_for :messages, allow_destroy: true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
conversation.messages_attributes = [
|
|
18
|
+
{ role: 'user', body: 'Hello' },
|
|
19
|
+
{ id: 'msg-1', body: 'Updated text' },
|
|
20
|
+
{ id: 'msg-2', _destroy: true }
|
|
21
|
+
]
|
|
22
|
+
conversation.save # atomic: all children created/updated/destroyed together
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **`Relation#loaded?`** — Check whether a has_many relation's records have been loaded into memory.
|
|
26
|
+
|
|
3
27
|
## 0.0.18
|
|
4
28
|
|
|
5
29
|
### Added
|
|
@@ -152,9 +152,92 @@ module ActiveItem
|
|
|
152
152
|
index_name => { partition_key: dynamo_key }
|
|
153
153
|
)
|
|
154
154
|
end
|
|
155
|
-
end
|
|
156
155
|
|
|
157
|
-
|
|
156
|
+
# Allows nested attributes for associated records, similar to Rails.
|
|
157
|
+
# Defines a writer method that builds or updates child records through the parent.
|
|
158
|
+
#
|
|
159
|
+
# @example
|
|
160
|
+
# class Conversation < ApplicationRecord
|
|
161
|
+
# has_many :messages
|
|
162
|
+
# accepts_nested_attributes_for :messages
|
|
163
|
+
# end
|
|
164
|
+
#
|
|
165
|
+
# conversation.messages_attributes = [
|
|
166
|
+
# { role: 'user', body: 'Hello' },
|
|
167
|
+
# { role: 'assistant', body: 'Hi there' }
|
|
168
|
+
# ]
|
|
169
|
+
# conversation.save # saves parent and creates children
|
|
170
|
+
#
|
|
171
|
+
# @param associations [Array<Symbol>] Association names to accept nested attributes for
|
|
172
|
+
# @param options [Hash] Options (allow_destroy: true to enable _destroy flag)
|
|
173
|
+
def accepts_nested_attributes_for(*associations, **options)
|
|
174
|
+
associations.each do |association_name|
|
|
175
|
+
define_nested_attributes_writer(association_name, options)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def define_nested_attributes_writer(association_name, options)
|
|
182
|
+
writer_method = :"#{association_name}_attributes="
|
|
183
|
+
allow_destroy = options.fetch(:allow_destroy, false)
|
|
184
|
+
|
|
185
|
+
# Store pending nested records for saving after parent
|
|
186
|
+
define_method(writer_method) do |attributes_collection|
|
|
187
|
+
attributes_collection = attributes_collection.values if attributes_collection.is_a?(Hash)
|
|
188
|
+
@_nested_attributes ||= {}
|
|
189
|
+
@_nested_attributes[association_name] = { records: attributes_collection, allow_destroy: allow_destroy }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Hook into save to persist nested records
|
|
193
|
+
define_method(:save_with_nested) do
|
|
194
|
+
result = save_without_nested
|
|
195
|
+
return result unless result
|
|
196
|
+
|
|
197
|
+
save_nested_attributes
|
|
198
|
+
result
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
define_method(:save_nested_attributes) do
|
|
202
|
+
return unless instance_variable_defined?(:@_nested_attributes) && @_nested_attributes
|
|
203
|
+
|
|
204
|
+
self.class.transaction do
|
|
205
|
+
@_nested_attributes.each do |assoc_name, config|
|
|
206
|
+
assoc_config = self.class._associations[assoc_name]
|
|
207
|
+
next unless assoc_config
|
|
208
|
+
|
|
209
|
+
target_class = safe_constantize_model(assoc_config[:class_name])
|
|
210
|
+
foreign_key = assoc_config[:foreign_key]
|
|
211
|
+
|
|
212
|
+
config[:records].each do |attrs|
|
|
213
|
+
attrs = attrs.transform_keys(&:to_sym)
|
|
214
|
+
|
|
215
|
+
if attrs[:_destroy] && config[:allow_destroy]
|
|
216
|
+
if attrs[:id]
|
|
217
|
+
record = target_class.find(attrs[:id])
|
|
218
|
+
record.destroy!
|
|
219
|
+
end
|
|
220
|
+
elsif attrs[:id]
|
|
221
|
+
record = target_class.find(attrs[:id])
|
|
222
|
+
record.assign_attributes(attrs.except(:id, :_destroy))
|
|
223
|
+
record.save!
|
|
224
|
+
else
|
|
225
|
+
target_class.create!(attrs.merge(foreign_key.to_sym => id))
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
@_nested_attributes = nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Wrap save to include nested attributes
|
|
235
|
+
return if method_defined?(:save_without_nested)
|
|
236
|
+
|
|
237
|
+
alias_method :save_without_nested, :save
|
|
238
|
+
alias_method :save, :save_with_nested
|
|
239
|
+
end
|
|
240
|
+
end
|
|
158
241
|
|
|
159
242
|
def load_has_many_association(name)
|
|
160
243
|
config = self.class._associations[name]
|
data/lib/active_item/relation.rb
CHANGED
|
@@ -31,6 +31,11 @@ module ActiveItem
|
|
|
31
31
|
@select_attributes = select_attributes # Projection expression attributes
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Whether the relation's records have been loaded from DynamoDB.
|
|
35
|
+
def loaded?
|
|
36
|
+
@loaded
|
|
37
|
+
end
|
|
38
|
+
|
|
34
39
|
# Chainable includes - preload associations to avoid N+1 queries
|
|
35
40
|
#
|
|
36
41
|
# Supports three forms:
|
|
@@ -50,5 +50,47 @@ module ActiveItem
|
|
|
50
50
|
def validates_uniqueness_of(*attributes, **options)
|
|
51
51
|
validates(*attributes, uniqueness: options.empty? || options)
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
# Validates that the associated records are valid.
|
|
55
|
+
# If any associated record is invalid, errors are added to the parent.
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# class Conversation < ApplicationRecord
|
|
59
|
+
# has_many :messages
|
|
60
|
+
# validates_associated :messages
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# conversation.messages.build(role: "invalid")
|
|
64
|
+
# conversation.valid? # => false
|
|
65
|
+
# conversation.errors[:messages] # => ["is invalid"]
|
|
66
|
+
def validates_associated(*associations, **options)
|
|
67
|
+
validates(*associations, associated: options.empty? || options)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ActiveModel validator that checks associated records are valid.
|
|
72
|
+
# Validates in-memory built records (via .build) and already-loaded records.
|
|
73
|
+
# Does not trigger a DB query — only checks what's already in memory.
|
|
74
|
+
class AssociatedValidator < ActiveModel::EachValidator
|
|
75
|
+
def validate_each(record, attribute, _value)
|
|
76
|
+
association_config = record.class._associations[attribute]
|
|
77
|
+
return unless association_config
|
|
78
|
+
|
|
79
|
+
associated = record.send(attribute)
|
|
80
|
+
|
|
81
|
+
# Only validate loaded/cached records — don't trigger a DB query
|
|
82
|
+
records = if associated.respond_to?(:loaded?) && associated.loaded?
|
|
83
|
+
associated.to_a
|
|
84
|
+
else
|
|
85
|
+
[]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
records.each do |child|
|
|
89
|
+
next if child.valid?
|
|
90
|
+
|
|
91
|
+
record.errors.add(attribute, :invalid, **options.except(:on))
|
|
92
|
+
break
|
|
93
|
+
end
|
|
94
|
+
end
|
|
53
95
|
end
|
|
54
96
|
end
|
data/lib/active_item/version.rb
CHANGED