Module: StandardLedger::Entry
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/standard_ledger/entry.rb
Overview
Marks an ActiveRecord model as a ledger entry: an immutable, append-only row that may project onto one or more aggregate targets.
Including this concern installs:
- the `ledger_entry` class macro (declares immutability + idempotency)
- read-only behavior post-creation (when `immutable: true`, the default)
- idempotency-by-unique-index (when `idempotency_key:` is non-nil)
Projection registration happens via the separate ‘Projector` concern —the two are decoupled so that an entry can be marked immutable without also opting into projections, and vice versa.
Instance Method Summary collapse
-
#destroy ⇒ Object
Wrap ‘destroy` so it can bypass the `readonly?` guard when the entry has opted in via `allow_destroy: true`.
-
#idempotent? ⇒ Boolean
Returns true when this row was returned from an idempotent ‘create!` rescue — i.e.
-
#readonly? ⇒ Boolean
AR consults ‘readonly?` from `save`/`update`/`destroy` paths; raising ReadOnlyRecord here matches the ActiveRecord contract for persisted immutable rows.
-
#standard_ledger_targets ⇒ Hash{Symbol => ActiveRecord::Base}
Returns the entry’s belongs_to targets keyed by association name.
Instance Method Details
#destroy ⇒ Object
Wrap ‘destroy` so it can bypass the `readonly?` guard when the entry has opted in via `allow_destroy: true`. This applies to `destroy`, `destroy!`, and `dependent: :destroy` cascades from a parent record (all routes call through `#destroy`).
265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/standard_ledger/entry.rb', line 265 def destroy return super unless self.class.respond_to?(:standard_ledger_entry_config) config = self.class.standard_ledger_entry_config return super if config.nil? || !config[:immutable] || !config[:allow_destroy] @_standard_ledger_destroying = true super ensure @_standard_ledger_destroying = false end |
#idempotent? ⇒ Boolean
Returns true when this row was returned from an idempotent ‘create!` rescue — i.e. an existing row matched the unique constraint and was returned instead of inserted.
239 240 241 |
# File 'lib/standard_ledger/entry.rb', line 239 def idempotent? !!@_standard_ledger_idempotent end |
#readonly? ⇒ Boolean
AR consults ‘readonly?` from `save`/`update`/`destroy` paths; raising ReadOnlyRecord here matches the ActiveRecord contract for persisted immutable rows. New, unpersisted instances stay writable so the initial INSERT can land.
When ‘allow_destroy: true` is set, `#destroy` toggles `@_standard_ledger_destroying` so `readonly?` returns false for the duration of the destroy call (and the duration of any cascade destroys that fire from its `dependent: :destroy` associations). The save/update path is unaffected — those still raise on persisted rows.
254 255 256 257 258 259 |
# File 'lib/standard_ledger/entry.rb', line 254 def readonly? return super unless standard_ledger_immutable? return false if @_standard_ledger_destroying !new_record? end |
#standard_ledger_targets ⇒ Hash{Symbol => ActiveRecord::Base}
Returns the entry’s belongs_to targets keyed by association name. Used by the ‘entry.created` notification payload and by `StandardLedger.post`’s telemetry. Skips polymorphic and missing associations so the payload only includes what’s actually present.
Performance trade-off: this fires from ‘after_commit`, where AR may have cleared the association cache. Each `public_send(reflection.name)` can therefore issue a SELECT to reload the cached target. For the typical 1–2 belongs_to entry, that’s negligible. If profiling on a high-cardinality entry shows this matters, capture targets earlier (e.g. in ‘before_create`) and stash them on the instance — deferred to a future PR. Notably, an inline-mode caller has already resolved these targets by the time `after_commit` runs, so the SELECTs would only happen for entries with belongs_to associations that are not registered as projection targets.
294 295 296 297 298 299 300 301 302 303 |
# File 'lib/standard_ledger/entry.rb', line 294 def standard_ledger_targets return {} unless self.class.respond_to?(:reflect_on_all_associations) self.class.reflect_on_all_associations(:belongs_to).each_with_object({}) do |reflection, memo| next if reflection.polymorphic? target = public_send(reflection.name) memo[reflection.name] = target unless target.nil? end end |