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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a4b1c7cc7af49e67776f895886bdf472d1e801ab3280f3ea28101bd20b8a701
4
- data.tar.gz: 7415931df8a26b187e4a9ab52586b67c7cea08f0fa3a476ab09b863c02250835
3
+ metadata.gz: 89a17fbb7cce2459ac2ab8c4edf9e981e12193571b273c149b3d2d74511dba9e
4
+ data.tar.gz: 1cb5a9301704de43df7fd601c841183bbfdb5b140bac9e2f7ff2b4e729b77f70
5
5
  SHA512:
6
- metadata.gz: b8c97c1ffadb921d440702b5299cf4bb219ba97ad0351cef7bebfea1125e74bf027ddc0546e1a254da8d6da8952598bf170368404c6ab65074ecc8379bfaec3b
7
- data.tar.gz: b9936169d15857e1423de8f622ddfe85b7be05458739f569ce1983dd6565f234ae2789f12978ac530106cc98203906a99b9666805d1e2e43c7e0a275e7c41d06
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
@@ -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 StandardError, "Validation failed: #{errors.full_messages.join(', ')}" unless save
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
- yield txn
389
- txn.execute!
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
- destroy || raise(RecordNotDestroyed.new(nil, self))
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveItem
4
- VERSION = '0.0.17'
4
+ VERSION = '0.0.18'
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.17
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-04 00:00:00.000000000 Z
12
+ date: 2026-08-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activemodel