mirror of
https://github.com/we-promise/sure.git
synced 2026-09-07 15:44:21 +00:00
* fix(rules): validate Rule::Condition type registry + normalize legacy 'name' values Closes #1229. /rules crashes with `ActionView::Template::Error (Unsupported condition type: name)` when a Rule::Condition row has a condition_type the registry doesn't know about. Reporter found two cases in the wild: - "name" — invalid, crashes the rules index because the view path (`_rule.html.erb` → `displayed_condition.filter.label`) ends up in `Rule::Registry#get_filter!` which raises. - "transaction_details" — actually a valid filter that searches the `transactions.extra` JSONB metadata field (see existing tests in test/models/rule/condition_test.rb). Reporter assumed it was a transaction-name synonym; it isn't. This PR leaves it alone. Changes: - Rule::Condition gains a SUPPORTED_CONDITION_TYPES registry constant matching Rule::Registry::TransactionResource#condition_filters plus "compound", an inclusion validation against it, and a before_validation callback that maps the one known legacy alias ("name" -> "transaction_name") so saving an existing rule fixes itself. - Rule::Condition#filter now rescues UnsupportedConditionError and returns a Rule::ConditionFilter::Unsupported placeholder. The placeholder labels itself "Unsupported (<key>)" (i18n) and its #apply returns `scope.none`, so any stale row that survives the migration stops matching rather than silently matching everything. - A one-shot migration normalizes existing "name" rows. Single UPDATE statement — rule_conditions is a small per-family table. Tests added in test/models/rule/condition_test.rb cover the inclusion validation, the normalization callback, and the graceful-render path (uses update_columns to simulate a row written before the validation existed — that's the exact codepath that crashes /rules today). * test(rules): add system test for unsupported condition_type render + raise on migration down - test/system/rules_test.rb: visit /rules with a row whose condition_type has been update_columns'd to "name" — assert page renders and shows the "Unsupported (name)" label instead of raising. - db/migrate/...normalize_rule_condition_types.rb: replace the empty #down with `raise ActiveRecord::IrreversibleMigration` to match the repo's data-migration convention (see e.g. 20260219190000_scope_*). * chore(rules): log unsupported condition + cross-check supported types Address maintainer review on #1908: - Log a Rails.logger.warn (with rule_id and condition_type) from Rule::ConditionFilter::Unsupported#apply so silent zero-match rules are traceable when debugging. - Add a comment above Rule::Condition::SUPPORTED_CONDITION_TYPES and a test that cross-checks it against the registry's filter keys, so drift between the two surfaces as a failing test rather than a confusing validation error. * refactor(rules): derive supported condition types from registry --------- Co-authored-by: John Baillie <johnbaillie2007@gmail.com> Co-authored-by: Khaostica <256858950+Khaostica@users.noreply.github.com> Co-authored-by: sure-admin <sure-admin@splashblot.com>
92 lines
2.3 KiB
Ruby
92 lines
2.3 KiB
Ruby
class Rule::Condition < ApplicationRecord
|
|
SUPPORTED_CONDITION_TYPES = (Rule::Registry::TransactionResource.condition_filter_keys + [ "compound" ]).freeze
|
|
|
|
LEGACY_CONDITION_TYPE_ALIASES = {
|
|
"name" => "transaction_name"
|
|
}.freeze
|
|
|
|
belongs_to :rule, touch: true, optional: -> { where.not(parent_id: nil) }
|
|
belongs_to :parent, class_name: "Rule::Condition", optional: true, inverse_of: :sub_conditions
|
|
|
|
has_many :sub_conditions, -> { order(:created_at, :id) }, class_name: "Rule::Condition", foreign_key: :parent_id, dependent: :destroy, inverse_of: :parent
|
|
|
|
before_validation :normalize_legacy_condition_type
|
|
|
|
validates :condition_type, presence: true, inclusion: { in: SUPPORTED_CONDITION_TYPES, allow_blank: true }
|
|
validates :operator, presence: true
|
|
validates :value, presence: true, unless: -> { compound? || operator == "is_null" }
|
|
|
|
accepts_nested_attributes_for :sub_conditions, allow_destroy: true
|
|
|
|
# We don't store rule_id on sub_conditions, so "walk up" to the parent rule
|
|
def rule
|
|
parent&.rule || super
|
|
end
|
|
|
|
def compound?
|
|
condition_type == "compound"
|
|
end
|
|
|
|
def apply(scope)
|
|
if compound?
|
|
build_compound_scope(scope)
|
|
else
|
|
filter.apply(scope, operator, value)
|
|
end
|
|
end
|
|
|
|
def prepare(scope)
|
|
if compound?
|
|
sub_conditions.reduce(scope) { |s, sub| sub.prepare(s) }
|
|
else
|
|
filter.prepare(scope)
|
|
end
|
|
end
|
|
|
|
def value_display
|
|
if value.present?
|
|
if options
|
|
options.find { |option| option.last == value }&.first
|
|
else
|
|
value
|
|
end
|
|
else
|
|
""
|
|
end
|
|
end
|
|
|
|
def options
|
|
filter.options
|
|
end
|
|
|
|
def operators
|
|
filter.operators
|
|
end
|
|
|
|
def filter
|
|
rule.registry.get_filter!(condition_type)
|
|
rescue Rule::Registry::UnsupportedConditionError
|
|
Rule::ConditionFilter::Unsupported.new(rule, condition_type)
|
|
end
|
|
|
|
private
|
|
def normalize_legacy_condition_type
|
|
return if condition_type.blank?
|
|
|
|
normalized = LEGACY_CONDITION_TYPE_ALIASES[condition_type]
|
|
self.condition_type = normalized if normalized
|
|
end
|
|
|
|
def build_compound_scope(scope)
|
|
if operator == "or"
|
|
combined_scope = sub_conditions
|
|
.map { |sub| sub.apply(scope) }
|
|
.reduce { |acc, s| acc.or(s) }
|
|
|
|
combined_scope || scope
|
|
else
|
|
sub_conditions.reduce(scope) { |s, sub| sub.apply(s) }
|
|
end
|
|
end
|
|
end
|