forest_admin_agent 1.44.1 → 1.44.2

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: 7685f99576db0d5421c02e2ccd0024cbdf8cdae97c50b81c273347058d01919c
4
- data.tar.gz: ca1838fe30e5619908f3aa9364a2f98181d78bd86078c49fb4f3202b3dc606af
3
+ metadata.gz: eb144d36d50f4763b0b8f2e61e701ee5fbfb7a97b66b4a9a6f67aaff26b8ac5d
4
+ data.tar.gz: c6f7817f4a8507b703c604b8c4c643884eb57b8d0902cd90437e6ffe1b201238
5
5
  SHA512:
6
- metadata.gz: c0eb71e1942b23f47f48c1bc211a2fbfdfd7eebe11e9c8e724feea421bca97df3864490a7d5d5f57bae497eff2705334026210d365131e6e41b79aa9ee62cd1f
7
- data.tar.gz: 87e7c2a7af1f695f6cd92f61e8d71f21962ca16db50b6085525395bc920c9256cf78b2405702b7eeb9ac124bfa6ac00191b790edab8a9168f6013b2ec99c5970
6
+ metadata.gz: c76840f3a4d7823a6395b6926e5ba60cb018f8acc30f6166773334ce6e966292b410001ad262a26cf0ca36580bf08dfcd0c0451821f52552919c37ec242083a7
7
+ data.tar.gz: fd08b9086757654dd821a909376648fa0f1a7274b18118cf734573bfc99f1818fabc063f085c72d1bccd7d94a8852f299aa0cd23703a3588d55f81a33a12f03b
data/AUDIT_TRAIL.md CHANGED
@@ -199,8 +199,21 @@ untouched. The row itself is always returned: what happened, by whom and when st
199
199
 
200
200
  A snapshot only answers for the writable columns it captured, so a scope reaching for anything else — a
201
201
  read-only column, a relation, a value stored redacted — is not evaluated against it at all and the values
202
- are withheld: `nil` there is a missing answer, not a passing one. Primary keys are the exception, read
203
- back from the row's own id, so a scope on the id still matches the record it belongs to. This is also
202
+ are withheld: `nil` there is a missing answer, not a passing one, and a redacted value answers no better.
203
+ Primary keys are the exception: the packed id fills in whatever the snapshot cannot answer — a read-only key
204
+ it never captured, or a writable one the trail redacts — so a scope on the id still matches the record it
205
+ belongs to. It fills in only what is missing: where a side captured the key itself, that value is the one
206
+ that was true there. Each side of an update is read against the id it was filed under, the row's own id and,
207
+ for the previous side of an update that moved the key, the id it moved from. A `pending` row is filed under
208
+ the id the record had *before* the write, which may not have landed, so its new side is given no id to fill
209
+ from and answers with what it captured or not at all.
210
+
211
+ Neither failure this can meet is fatal, and each costs only what it actually broke. An id that stopped
212
+ decoding when the primary key changed shape costs the keys it would have filled and nothing more, so a scope
213
+ that never asks about the id is still answered from the columns the row captured. A column captured as `nil`
214
+ that an ordered operator cannot compare withholds that row. Uncaught, either would have failed the whole
215
+ page, and only for the callers a scope applies to, since an unscoped caller never reaches this test. Both
216
+ are logged, and the row keeps its operation, author and timestamp. This is also
204
217
  what makes an update's partial diff safe to test: a diff that never carried the scoped column answers for
205
218
  neither side, so both are withheld.
206
219
 
@@ -114,9 +114,9 @@ module ForestAdminAgent
114
114
  case entry.operation
115
115
  # `delete`'s previous_values and `create`'s new_values both capture every writable column.
116
116
  when 'delete'
117
- in_scope?(entry, entry.previous_values, scope, context) ? entry : blank(entry, :previous_values)
117
+ in_scope?(entry.previous_values, entry.record_id, scope, context) ? entry : blank(entry, :previous_values)
118
118
  when 'create'
119
- in_scope?(entry, entry.new_values, scope, context) ? entry : blank(entry, :new_values)
119
+ in_scope?(entry.new_values, entry.record_id, scope, context) ? entry : blank(entry, :new_values)
120
120
  when 'update'
121
121
  withhold_each_side(entry, scope, context)
122
122
  # `action`/`action_failed` rows hold a submitted form and a result summary, not column values, so
@@ -131,30 +131,61 @@ module ForestAdminAgent
131
131
  # separately releases the ones that can be proven in scope — "it used to be X" can't escape through a
132
132
  # row whose new value is out of scope, since that side is tested on its own.
133
133
  def withhold_each_side(entry, scope, context)
134
- kept = in_scope?(entry, entry.previous_values, scope, context) ? entry : blank(entry, :previous_values)
134
+ # An update that moved a writable primary key files its row under the id the record ended up with,
135
+ # and keeps the one it had on `previous_record_id`. Each side is tested against the id it was true
136
+ # of, or the new state's id would decide whether the old state is in scope.
137
+ before = entry.previous_record_id || entry.record_id
138
+ kept = in_scope?(entry.previous_values, before, scope, context) ? entry : blank(entry, :previous_values)
135
139
 
136
- in_scope?(kept, kept.new_values, scope, context) ? kept : blank(kept, :new_values)
140
+ in_scope?(kept.new_values, after_id(kept), scope, context) ? kept : blank(kept, :new_values)
141
+ end
142
+
143
+ # A pending row is filed under the id the record had *before* the write, since the write may not have
144
+ # landed: it says nothing about the state the update was moving to, so the new side gets no id to fill
145
+ # from and falls back on what it captured itself.
146
+ def after_id(entry)
147
+ entry.status == ::ForestAdminAgent::AuditTrail::Recording::PENDING ? nil : entry.record_id
137
148
  end
138
149
 
139
150
  # Only a snapshot that answers every field the scope asks about, with what was really stored, is worth
140
151
  # matching. The capture keeps the writable columns, so a scope on anything else — a read-only column, a
141
152
  # relation — reads as nil there and would answer for a value the row never held: `status != 'private'`
142
153
  # would match, and an ordered operator would raise on the nil. A redacted value answers no better.
143
- def in_scope?(entry, values, scope, context)
144
- snapshot = with_primary_keys(entry, values, context.collection)
145
- answerable = scope.projection.all? do |field|
146
- snapshot.key?(field) && snapshot[field] != ::ForestAdminAgent::AuditTrail::Recording::REDACTED
147
- end
148
-
149
- answerable && scope.match(snapshot, context.collection, context.caller.timezone)
154
+ def in_scope?(values, packed_id, scope, context)
155
+ snapshot = answerable_snapshot(values, packed_id, context.collection)
156
+ return false unless scope.projection.all? { |field| snapshot.key?(field) }
157
+
158
+ scope.match(snapshot, context.collection, context.caller.timezone)
159
+ rescue StandardError => e
160
+ # Key presence is not answerability: a column captured as nil has its key, and an ordered operator
161
+ # raises on it. Uncaught that would fail the whole page, and only for the callers a scope applies
162
+ # to. One withheld row is the smaller loss, and the same answer the field would have got had it
163
+ # been missing outright.
164
+ Facades::Container.logger&.log('Warn', "[ForestAdmin] Audit row not scope-checkable: #{e.message}")
165
+
166
+ false
150
167
  end
151
168
 
152
- # A read-only primary key never lands in the snapshot, so a scope on the id would redact a row that is
153
- # squarely in scope. The row's own packed id carries those values.
154
- def with_primary_keys(entry, values, collection)
155
- keys = entry.record_id.nil? ? {} : Utils::Id.unpack_id(collection, entry.record_id, with_key: true)
169
+ # What this side of the row can answer about. A redacted value answers nothing, so it is dropped rather
170
+ # than matched against the placeholder — leaving the field unanswered, which withholds. The packed id
171
+ # then fills in the primary keys: a read-only one never lands in the snapshot at all, and a writable one
172
+ # the trail redacts was just dropped, while the id the row was filed under proves what the key was.
173
+ # It only fills what the snapshot cannot answer: on the side of a row that captured the key itself,
174
+ # that value is the one that was true there.
175
+ def answerable_snapshot(values, packed_id, collection)
176
+ answered = (values || {}).reject { |_, value| value == ::ForestAdminAgent::AuditTrail::Recording::REDACTED }
177
+ return answered if packed_id.nil?
156
178
 
157
- keys.merge(values || {})
179
+ begin
180
+ Utils::Id.unpack_id(collection, packed_id, with_key: true).merge(answered)
181
+ rescue StandardError => e
182
+ # An id written under a primary key of another shape costs the keys it would have filled, and
183
+ # nothing else: the snapshot still answers for the columns it captured, so a scope that never
184
+ # asks about the id is unaffected.
185
+ Facades::Container.logger&.log('Warn', "[ForestAdmin] Audit row id not decodable: #{e.message}")
186
+
187
+ answered
188
+ end
158
189
  end
159
190
 
160
191
  def blank(entry, *fields)
@@ -6,7 +6,7 @@ module ForestAdminAgent
6
6
  module Schema
7
7
  class SchemaEmitter
8
8
  LIANA_NAME = "agent-ruby"
9
- LIANA_VERSION = "1.44.1"
9
+ LIANA_VERSION = "1.44.2"
10
10
 
11
11
  def self.generate(datasource)
12
12
  datasource.collections
@@ -1,3 +1,3 @@
1
1
  module ForestAdminAgent
2
- VERSION = "1.44.1"
2
+ VERSION = "1.44.2"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_admin_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.44.1
4
+ version: 1.44.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthieu
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2026-09-23 00:00:00.000000000 Z
12
+ date: 2026-09-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport