Module: CrudComponents::LikeSpec
- Defined in:
- lib/crud_components/like_spec.rb
Overview
The declarative mini-language shared by filter like: and search_in:
case-insensitive contains across columns, joining associations as needed.
:title own column
%i[title subtitle] several own columns, OR-combined
{ authors: %i[name email] } join, explicit columns
:publisher join, search Publisher's label (what you see)
{ user: :address } nested join, search Address's label
Specs never contain SQL strings; conditions are built through Arel with LIKE wildcards escaped, so they are parameterized end to end.
Defined Under Namespace
Classes: Entry
Class Method Summary collapse
- .apply(scope, spec, value) ⇒ Object
- .deep_merge(left, right) ⇒ Object
-
.delegate(model, reflection, path) ⇒ Object
Association name without columns: search the target's label column — the name shown in its cell ("search what you see").
-
.expand(model, spec, path = []) ⇒ Object
Resolves a spec into flat [path, klass, column] entries.
-
.expand_assoc(model, assoc, sub, path) ⇒ Object
Explicit nesting ({ assoc => columns }) — spell the target's columns out.
- .expand_name(model, name, path) ⇒ Object
Class Method Details
.apply(scope, spec, value) ⇒ Object
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/crud_components/like_spec.rb', line 16 def apply(scope, spec, value) model = scope.respond_to?(:model) ? scope.model : scope entries = (model, spec) return scope if entries.empty? pattern = "%#{ActiveRecord::Base.sanitize_sql_like(value)}%" condition = entries.map { |entry| entry.arel_condition(pattern) }.reduce(:or) joins = entries.filter_map(&:join_fragment).reduce({}) { |acc, j| deep_merge(acc, j) } return scope.where(condition) if joins.empty? # A join can multiply rows, so the match has to be de-duplicated. We do it # with an id subquery rather than SELECT DISTINCT: DISTINCT compares every # selected column, which Postgres can't do for a json (or other # non-comparable) column the scope happens to select — it raises "could not # identify an equality operator for type json". The subquery sidesteps that # and keeps the join (and its rows) out of the outer query entirely. key = model.arel_table[model.primary_key] scope.where(key.in(model.left_joins(joins).where(condition).select(key).arel)) end |
.deep_merge(left, right) ⇒ Object
103 104 105 106 107 108 109 110 |
# File 'lib/crud_components/like_spec.rb', line 103 def deep_merge(left, right) normalize = ->(j) { j.is_a?(Hash) ? j : { j => {} } } l = normalize.call(left) normalize.call(right).each do |key, value| l[key] = l.key?(key) ? deep_merge(l[key], value) : value end l end |
.delegate(model, reflection, path) ⇒ Object
Association name without columns: search the target's label column — the name shown in its cell ("search what you see"). A block/computed label has no single column to match, so ask for the columns explicitly.
91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/crud_components/like_spec.rb', line 91 def delegate(model, reflection, path) target = reflection.klass label = Structure.for(target).label_field_name if label.nil? raise DefinitionError, "cannot search #{model}##{reflection.name} by label: " \ "#{target}'s label is a custom block, not a column — spell the columns out, " \ "e.g. { #{reflection.name}: %i[...] }" end [Entry.new(path + [reflection.name], target, label)] end |
.expand(model, spec, path = []) ⇒ Object
Resolves a spec into flat [path, klass, column] entries. A bare association name resolves to the target's label column — the text you actually see in that association's cell — so it can never reach a target's hidden columns (passwords, tokens, …) and there is no search_in chain to form a cycle.
56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/crud_components/like_spec.rb', line 56 def (model, spec, path = []) Array.wrap(spec).flat_map do |item| case item when Symbol, String then (model, item.to_sym, path) when Hash then item.flat_map { |assoc, sub| (model, assoc.to_sym, sub, path) } else raise DefinitionError, "invalid like-spec element #{item.inspect} for #{model} — " \ 'use column symbols, association symbols, or { assoc => columns } hashes' end end end |
.expand_assoc(model, assoc, sub, path) ⇒ Object
Explicit nesting ({ assoc => columns }) — spell the target's columns out.
80 81 82 83 84 85 86 |
# File 'lib/crud_components/like_spec.rb', line 80 def (model, assoc, sub, path) reflection = model.reflect_on_association(assoc) raise DefinitionError, "like-spec references association '#{assoc}', " \ "which #{model} does not have" unless reflection (reflection.klass, sub, path + [assoc]) end |
.expand_name(model, name, path) ⇒ Object
68 69 70 71 72 73 74 75 76 77 |
# File 'lib/crud_components/like_spec.rb', line 68 def (model, name, path) if model.columns_hash.key?(name.to_s) [Entry.new(path, model, name)] elsif (reflection = model.reflect_on_association(name)) delegate(model, reflection, path) else raise DefinitionError, "like-spec references '#{name}', which is neither a column nor " \ "an association of #{model}" end end |