forest_admin_agent 1.46.2 → 1.46.4
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/lib/forest_admin_agent/audit_trail/sql/migrations.rb +21 -0
- data/lib/forest_admin_agent/audit_trail/store.rb +5 -4
- data/lib/forest_admin_agent/routes/action/actions.rb +9 -0
- data/lib/forest_admin_agent/utils/schema/schema_emitter.rb +1 -1
- data/lib/forest_admin_agent/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: c7341653664e11b2a2815efabb1b73b6c5e10a2ca219eed25e36f16aab1b4c21
|
|
4
|
+
data.tar.gz: c9ce7a926b82aade66097096dbd68a98edf3bd1dcf7b3cf5a9ef79b47c2caa03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97395bd2edd71af60ef0db33ca0826f4c7c2d56059eb44766e34343c57e9064de22c5951d35b3e6a90d2299c1aa53dfa6d8e780fd56741cce6836cde86204fee
|
|
7
|
+
data.tar.gz: bb0a5bd3e978b957ae76361e84e42086c22d6cd377502463e584ef902aaffcedf838de135894ca8e89a51ca1c4c5996800543d5620b4aefa03370a001de2e3c2
|
|
@@ -62,6 +62,27 @@ module ForestAdminAgent
|
|
|
62
62
|
connection.add_index(table, %i[timestamp id], name: "#{table.split(".").last}_timestamp_id",
|
|
63
63
|
if_not_exists: true)
|
|
64
64
|
end
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
# A record's history is read by (collection, record_id) in (timestamp, id) order. With only
|
|
68
|
+
# `record_id` and `(timestamp, id)` to choose from, Postgres either reads every row the id holds
|
|
69
|
+
# across all collections, or walks the whole table's (timestamp, id) index under a LIMIT: the life
|
|
70
|
+
# boundary lookup discarded 2.1M rows (486 ms) on a 2.1M-row table for a record created long ago.
|
|
71
|
+
name: '004-index-record-history',
|
|
72
|
+
up: lambda do |connection, table|
|
|
73
|
+
base = table.split('.').last
|
|
74
|
+
columns = %i[collection record_id timestamp id]
|
|
75
|
+
options = { if_not_exists: true }
|
|
76
|
+
options[:length] = { record_id: 255 } if connection.adapter_name.downcase.match?(/mysql|maria/)
|
|
77
|
+
|
|
78
|
+
connection.add_index(table, columns, name: "#{base}_record_history", **options)
|
|
79
|
+
next unless connection.supports_partial_index?
|
|
80
|
+
|
|
81
|
+
# The life boundary keeps a record's `create` and `delete` rows only: without this, a record
|
|
82
|
+
# updated 100k times is read whole to find its one `create`.
|
|
83
|
+
connection.add_index(table, columns, name: "#{base}_life_boundaries",
|
|
84
|
+
where: "operation IN ('create', 'delete')", **options)
|
|
85
|
+
end
|
|
65
86
|
}
|
|
66
87
|
].freeze
|
|
67
88
|
end
|
|
@@ -204,11 +204,12 @@ module ForestAdminAgent
|
|
|
204
204
|
.uniq { |author| author[:user_id] }
|
|
205
205
|
end
|
|
206
206
|
|
|
207
|
-
# Rows strictly before `row` in the trail's (timestamp, id) order.
|
|
207
|
+
# Rows strictly before `row` in the trail's (timestamp, id) order. A row value rather than the equivalent
|
|
208
|
+
# `timestamp < ? OR (timestamp = ? AND id < ?)`, which Postgres cannot turn into a range on the
|
|
209
|
+
# (collection, record_id, timestamp, id) index: on a 50k-row history the OR read all of it (7 ms), the row
|
|
210
|
+
# value a single entry (0.06 ms).
|
|
208
211
|
def before_row_condition(row)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
['timestamp < ? OR (timestamp = ? AND id < ?)', at, at, row.id]
|
|
212
|
+
['(timestamp, id) < (?, ?)', as_time(row.timestamp), row.id]
|
|
212
213
|
end
|
|
213
214
|
|
|
214
215
|
# Rows strictly past `row` in the history's own order, so a scan keyed on the last row it read neither
|
|
@@ -127,6 +127,14 @@ module ForestAdminAgent
|
|
|
127
127
|
"The collection #{context.collection.name} does not have this smart action"
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# A single action runs on its first record, or on nothing: refuse any selection but one id.
|
|
131
|
+
def ensure_single_record(context, selection_ids)
|
|
132
|
+
return unless context.collection.schema[:actions][@action_name].scope == Types::ActionScope::SINGLE
|
|
133
|
+
return unless selection_ids[:are_excluded] || selection_ids[:ids].size != 1
|
|
134
|
+
|
|
135
|
+
raise Http::Exceptions::UnprocessableError, 'This action can only be triggered on one record at a time.'
|
|
136
|
+
end
|
|
137
|
+
|
|
130
138
|
# Recorded as pending before the action runs and confirmed after, so an action that takes the process
|
|
131
139
|
# down with it still leaves evidence that it started. A failed run is worth recording too — "who tried
|
|
132
140
|
# to run this" is usually the interesting part — and an action answering with an Error result failed
|
|
@@ -229,6 +237,7 @@ module ForestAdminAgent
|
|
|
229
237
|
# Restrict the filter to the selected records for single or bulk actions
|
|
230
238
|
if context.collection.schema[:actions][@action_name].scope != Types::ActionScope::GLOBAL
|
|
231
239
|
selection_ids = Utils::Id.parse_selection_ids(context.collection, args[:params])
|
|
240
|
+
ensure_single_record(context, selection_ids)
|
|
232
241
|
selected_ids = ConditionTreeFactory.match_ids(context.collection, selection_ids[:ids])
|
|
233
242
|
selected_ids = selected_ids.inverse if selection_ids[:are_excluded]
|
|
234
243
|
filter = filter.override(
|
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.46.
|
|
4
|
+
version: 1.46.4
|
|
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-10-
|
|
12
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activerecord
|