activeitem 0.0.17 → 0.0.18
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 +20 -0
- data/lib/active_item/base.rb +74 -4
- data/lib/active_item/errors.rb +11 -0
- data/lib/active_item/transaction.rb +34 -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: 89a17fbb7cce2459ac2ab8c4edf9e981e12193571b273c149b3d2d74511dba9e
|
|
4
|
+
data.tar.gz: 1cb5a9301704de43df7fd601c841183bbfdb5b140bac9e2f7ff2b4e729b77f70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e7ac3bb2d79938a0aec2d860fc1cd106472e8d24b27166dc0afccaba2fac740faed88d2753e664f96f3149937e6b0209c35d316bfa84b7c020e45e601fa8ab2
|
|
7
|
+
data.tar.gz: 9c9c2bac36a241539eab5ebc44dd428d3b91b2780b18dd1660e5b3a997fc151669ea46df521b29e5636e26e6f444063a4bf49d059940bf0d03f3bc5bfeb5928c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Transactional saves** — `save`, `save!`, `destroy`, and `destroy!` now automatically enroll in the current transaction when called inside a `Model.transaction` block. This enables an ActiveRecord-style implicit API:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
Model.transaction do
|
|
11
|
+
record1.save!
|
|
12
|
+
record2.save!
|
|
13
|
+
record3.destroy!
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
All operations are committed atomically at block end. The explicit API (`txn.put(record)`) continues to work for backwards compatibility.
|
|
18
|
+
|
|
19
|
+
- **RecordInvalid exception** — `save!` now raises `ActiveItem::RecordInvalid` with the record attached, instead of a generic `StandardError`. This matches ActiveRecord behavior.
|
|
20
|
+
|
|
21
|
+
- **Transaction.active?** — class method to check if code is executing inside a transaction block.
|
|
22
|
+
|
|
3
23
|
## 0.0.13
|
|
4
24
|
|
|
5
25
|
### Changed
|
data/lib/active_item/base.rb
CHANGED
|
@@ -349,6 +349,9 @@ module ActiveItem
|
|
|
349
349
|
def save(validate: true)
|
|
350
350
|
return false if validate && !run_validations
|
|
351
351
|
|
|
352
|
+
# If inside a transaction block, enroll this save in the transaction
|
|
353
|
+
return enroll_in_transaction if Transaction.active?
|
|
354
|
+
|
|
352
355
|
result = run_callbacks :save do
|
|
353
356
|
if new_record?
|
|
354
357
|
run_callbacks(:create) { perform_create }
|
|
@@ -368,7 +371,9 @@ module ActiveItem
|
|
|
368
371
|
end
|
|
369
372
|
|
|
370
373
|
def save!
|
|
371
|
-
raise
|
|
374
|
+
raise RecordInvalid.new(self), "Validation failed: #{errors.full_messages.join(', ')}" unless save
|
|
375
|
+
|
|
376
|
+
true
|
|
372
377
|
end
|
|
373
378
|
|
|
374
379
|
def self.create(attributes = {})
|
|
@@ -383,10 +388,39 @@ module ActiveItem
|
|
|
383
388
|
obj
|
|
384
389
|
end
|
|
385
390
|
|
|
391
|
+
# Execute a block within a transaction context.
|
|
392
|
+
#
|
|
393
|
+
# Supports two usage patterns:
|
|
394
|
+
#
|
|
395
|
+
# 1. Explicit API (block receives transaction):
|
|
396
|
+
# Model.transaction do |txn|
|
|
397
|
+
# txn.put(record1)
|
|
398
|
+
# txn.update(record2)
|
|
399
|
+
# end
|
|
400
|
+
#
|
|
401
|
+
# 2. Implicit API (transactional saves):
|
|
402
|
+
# Model.transaction do
|
|
403
|
+
# record1.save!
|
|
404
|
+
# record2.save!
|
|
405
|
+
# record3.destroy!
|
|
406
|
+
# end
|
|
407
|
+
#
|
|
408
|
+
# In the implicit API, save/destroy calls are automatically enrolled.
|
|
409
|
+
# The transaction is committed when the block completes successfully.
|
|
410
|
+
# If an exception is raised, no changes are committed (all-or-nothing).
|
|
411
|
+
#
|
|
412
|
+
# @yield [Transaction] the transaction object (optional)
|
|
413
|
+
# @raise [TransactionError] if the transaction fails
|
|
386
414
|
def self.transaction
|
|
387
415
|
txn = Transaction.new
|
|
388
|
-
|
|
389
|
-
|
|
416
|
+
Transaction.current = txn
|
|
417
|
+
begin
|
|
418
|
+
# Support both explicit (yield txn) and implicit (no block param) APIs
|
|
419
|
+
yield txn if block_given?
|
|
420
|
+
txn.execute!
|
|
421
|
+
ensure
|
|
422
|
+
Transaction.current = nil
|
|
423
|
+
end
|
|
390
424
|
end
|
|
391
425
|
|
|
392
426
|
def self.transaction_find(items)
|
|
@@ -408,6 +442,9 @@ module ActiveItem
|
|
|
408
442
|
end
|
|
409
443
|
|
|
410
444
|
def destroy
|
|
445
|
+
# If inside a transaction block, enroll this destroy in the transaction
|
|
446
|
+
return enroll_destroy_in_transaction if Transaction.active?
|
|
447
|
+
|
|
411
448
|
result = run_callbacks(:destroy) { perform_destroy }
|
|
412
449
|
return false if result == false
|
|
413
450
|
|
|
@@ -421,7 +458,9 @@ module ActiveItem
|
|
|
421
458
|
end
|
|
422
459
|
|
|
423
460
|
def destroy!
|
|
424
|
-
|
|
461
|
+
raise RecordNotDestroyed.new(nil, self) unless destroy
|
|
462
|
+
|
|
463
|
+
true
|
|
425
464
|
end
|
|
426
465
|
|
|
427
466
|
def delete
|
|
@@ -460,6 +499,37 @@ module ActiveItem
|
|
|
460
499
|
|
|
461
500
|
private
|
|
462
501
|
|
|
502
|
+
# Enroll this record's save operation in the current transaction.
|
|
503
|
+
# Called when save is invoked inside a transaction block.
|
|
504
|
+
def enroll_in_transaction
|
|
505
|
+
txn = Transaction.current
|
|
506
|
+
|
|
507
|
+
result = run_callbacks :save do
|
|
508
|
+
if new_record?
|
|
509
|
+
run_callbacks(:create) { txn.put(self) }
|
|
510
|
+
else
|
|
511
|
+
run_callbacks(:update) { txn.update(self) }
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
return false if result == false
|
|
516
|
+
|
|
517
|
+
# Mark changes as applied (will be committed when transaction executes)
|
|
518
|
+
# Note: @new_record stays true until transaction.execute! completes
|
|
519
|
+
true
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Enroll this record's destroy operation in the current transaction.
|
|
523
|
+
# Called when destroy is invoked inside a transaction block.
|
|
524
|
+
def enroll_destroy_in_transaction
|
|
525
|
+
txn = Transaction.current
|
|
526
|
+
|
|
527
|
+
result = run_callbacks(:destroy) { txn.delete(self) }
|
|
528
|
+
return false if result == false
|
|
529
|
+
|
|
530
|
+
true
|
|
531
|
+
end
|
|
532
|
+
|
|
463
533
|
def generate_primary_key
|
|
464
534
|
@id = nil if @id.to_s.strip.empty?
|
|
465
535
|
@id ||= SecureRandom.uuid
|
data/lib/active_item/errors.rb
CHANGED
|
@@ -4,6 +4,17 @@ module ActiveItem
|
|
|
4
4
|
class RecordNotFound < StandardError; end
|
|
5
5
|
class TransactionError < StandardError; end
|
|
6
6
|
|
|
7
|
+
# Raised by save! when validations fail.
|
|
8
|
+
class RecordInvalid < StandardError
|
|
9
|
+
attr_reader :record
|
|
10
|
+
|
|
11
|
+
def initialize(record = nil)
|
|
12
|
+
@record = record
|
|
13
|
+
message = record ? "Validation failed: #{record.errors.full_messages.join(', ')}" : 'Validation failed'
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
7
18
|
# Raised when an IAM policy denies a DynamoDB operation on a table.
|
|
8
19
|
class AccessDeniedError < StandardError
|
|
9
20
|
attr_reader :model_name, :table, :operation, :original_error
|
|
@@ -3,15 +3,49 @@
|
|
|
3
3
|
module ActiveItem
|
|
4
4
|
# Wraps DynamoDB TransactWriteItems, allowing multiple put, update, and
|
|
5
5
|
# delete operations to be committed atomically (up to 100 items).
|
|
6
|
+
#
|
|
7
|
+
# Supports two usage patterns:
|
|
8
|
+
#
|
|
9
|
+
# 1. Explicit API (original):
|
|
10
|
+
# Model.transaction do |txn|
|
|
11
|
+
# txn.put(record1)
|
|
12
|
+
# txn.update(record2)
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# 2. Implicit API (transactional saves):
|
|
16
|
+
# Model.transaction do
|
|
17
|
+
# record1.save!
|
|
18
|
+
# record2.save!
|
|
19
|
+
# record3.destroy!
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# In the implicit API, save/destroy calls inside the block are automatically
|
|
23
|
+
# enrolled in the transaction and committed atomically at block end.
|
|
6
24
|
class Transaction
|
|
7
25
|
MAX_ITEMS = 100
|
|
8
26
|
|
|
9
27
|
attr_reader :operations
|
|
10
28
|
|
|
29
|
+
class << self
|
|
30
|
+
# Thread-local storage for the current transaction context
|
|
31
|
+
def current
|
|
32
|
+
Thread.current[:activeitem_current_transaction]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def current=(txn)
|
|
36
|
+
Thread.current[:activeitem_current_transaction] = txn
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
11
40
|
def initialize
|
|
12
41
|
@operations = []
|
|
13
42
|
end
|
|
14
43
|
|
|
44
|
+
# Check if we're inside a transaction block
|
|
45
|
+
def self.active?
|
|
46
|
+
!current.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
15
49
|
def put(record, condition: nil)
|
|
16
50
|
record.instance_variable_set(:@id, SecureRandom.uuid) unless record.id
|
|
17
51
|
pk = record.class.primary_key
|
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.18
|
|
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-05 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activemodel
|