Class: ForestAdminAgent::AuditTrail::Sql::TextSearch

Inherits:
Object
  • Object
show all
Defined in:
lib/forest_admin_agent/audit_trail/sql/text_search.rb

Overview

SQL keeping only the audit entries a free-text term matches, case-insensitively and as a substring: the action's name, who acted, and the keys and values recorded on both sides of the change — at any depth, since only the changed leaves of a JSON column are stored and the term has to reach them.

Deliberately not searched: operation, correlation_key, record_id, collection, status and timestamp. Machine identifiers nobody searches for, and matching them turns one term into a pile of confusing hits.

The values are matched against the JSON document as text, which is what lets one condition reach any depth and compose with pagination and the count. It cannot use an index, which is affordable here because a history query is already narrowed to one record.

Constant Summary collapse

TEXT_COLUMNS =
%w[action_name user_first_name user_last_name user_email].freeze
JSON_COLUMNS =
%w[previous_values new_values].freeze
ESCAPE =

! rather than a backslash: MySQL treats a backslash as an escape inside string literals too, so ESCAPE '\' needs doubling there and nowhere else.

'!'.freeze
REDACTED =

A masked value is stored as this. It is removed before matching, so a search for "redacted" cannot hit it — and since the real value was never recorded, searching that finds nothing either. A search must never confirm a value the trail refused to keep.

Recording::REDACTED

Instance Method Summary collapse

Constructor Details

#initialize(connection) ⇒ TextSearch

Returns a new instance of TextSearch.



28
29
30
# File 'lib/forest_admin_agent/audit_trail/sql/text_search.rb', line 28

def initialize(connection)
  @connection = connection
end

Instance Method Details

#condition(term) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
# File 'lib/forest_admin_agent/audit_trail/sql/text_search.rb', line 32

def condition(term)
  text = term.to_s.downcase
  # The value objects are matched as serialized JSON, where a quote, a backslash or a newline is
  # escaped — so `15" monitor` sits in the document as `15\" monitor` and the raw term would never
  # find it. Escaping the term the same way makes it match, and stops a bare quote from matching the
  # document's own structure.
  clauses = TEXT_COLUMNS.map { |column| like(column, text) }
  clauses += JSON_COLUMNS.map { |column| like(searchable_json(column), json_escaped(text)) }

  clauses.join(' OR ')
end