Module: TypedEAV::HasTypedEAV::ClassQueryMethods
- Defined in:
- lib/typed_eav/has_typed_eav.rb
Overview
──────────────────────────────────────────────────Class-level query methods ──────────────────────────────────────────────────
Constant Summary collapse
- UNSET_SCOPE =
Sentinel for the ‘scope:` kwarg default. Distinguishes “kwarg not passed → resolve from ambient” (UNSET_SCOPE) from “explicitly nil →filter to global-only fields” (preserves prior behavior).
Object.new.freeze
- ALL_SCOPES =
Sentinel returned by ‘resolve_scope` inside an `unscoped { }` block. Signals the caller to skip the scope filter entirely (return fields across all partitions, not just global).
Object.new.freeze
Instance Method Summary collapse
-
#typed_eav_definitions(scope: UNSET_SCOPE) ⇒ Object
Returns field definitions for this entity type.
-
#where_typed_eav(*filters, scope: UNSET_SCOPE) ⇒ Object
Query by custom field values.
-
#with_field(name, operator_or_value = nil, value = nil, scope: UNSET_SCOPE) ⇒ Object
Shorthand for single-field queries.
Instance Method Details
#typed_eav_definitions(scope: UNSET_SCOPE) ⇒ Object
Returns field definitions for this entity type.
‘scope:` behavior:
- omitted → resolve from ambient (`with_scope` → resolver → raise/nil)
- passed a value → use verbatim (explicit override; admin/test path)
- passed nil → filter to global-only fields (prior behavior preserved)
230 231 232 233 234 235 236 237 |
# File 'lib/typed_eav/has_typed_eav.rb', line 230 def typed_eav_definitions(scope: UNSET_SCOPE) resolved = resolve_scope(scope) if resolved.equal?(ALL_SCOPES) TypedEAV::Field::Base.where(entity_type: name) else TypedEAV::Field::Base.for_entity(name, scope: resolved) end end |
#where_typed_eav(*filters, scope: UNSET_SCOPE) ⇒ Object
Query by custom field values. Accepts an array of filter hashes or a hash of hashes (from form params).
Each filter needs:
:name or :n - the field name
:op or :operator - the operator (default: :eq)
:value or :v - the comparison value
Contact.where_typed_eav(
{ name: "age", op: :gt, value: 21 },
{ name: "city", value: "Portland" } # op defaults to :eq
)
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity – input normalization + multimap branch + filter dispatch genuinely belong together; splitting hurts readability of the scope-collision logic.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/typed_eav/has_typed_eav.rb', line 120 def where_typed_eav(*filters, scope: UNSET_SCOPE) # Normalize input: accept splat args, a single array, a single filter hash, # a hash-of-hashes (form params), or ActionController::Parameters. filters = filters.map { |f| f.respond_to?(:to_unsafe_h) ? f.to_unsafe_h : f } if filters.size == 1 inner = filters.first inner = inner.to_unsafe_h if inner.respond_to?(:to_unsafe_h) if inner.is_a?(Array) filters = inner elsif inner.is_a?(Hash) # A single filter hash has keys like :name/:n, :op, :value/:v. # A hash-of-hashes (form params) has values that are all hashes. filter_keys = %i[name n op operator value v].map(&:to_s) filters = if inner.keys.any? { |k| filter_keys.include?(k.to_s) } [inner] else inner.values end end end filters = Array(filters) # Resolve the scope once so we can branch on whether we're inside # `TypedEAV.unscoped { }` (ALL_SCOPES) or a normal single-scope # query. Under ALL_SCOPES the same name can legitimately appear # across multiple tenant partitions; collapsing to one definition # would silently drop all but one tenant's matches. See the # multimap branch below. resolved = resolve_scope(scope) all_scopes = resolved.equal?(ALL_SCOPES) defs = if all_scopes TypedEAV::Field::Base.where(entity_type: name) else TypedEAV::Field::Base.for_entity(name, scope: resolved) end if all_scopes fields_multimap = HasTypedEAV.definitions_multimap_by_name(defs) filters.inject(all) do |query, filter| filter = filter.to_h.with_indifferent_access name = filter[:n] || filter[:name] operator = (filter[:op] || filter[:operator] || :eq).to_sym value = filter.key?(:v) ? filter[:v] : filter[:value] matching_fields = fields_multimap[name.to_s] unless matching_fields&.any? raise ArgumentError, "Unknown typed field '#{name}' for #{self.name}. " \ "Available fields: #{fields_multimap.keys.join(", ")}" end # OR-across all field_ids that share this name (across tenants), # while preserving AND between filters via the chained `.where`. # Use the underlying Value scope (`.filter(...)`) and pluck # entity_ids — `entity_ids` returns a relation, and pluck collapses # it to a plain integer array we can union across tenants. union_ids = matching_fields.flat_map do |f| TypedEAV::QueryBuilder.filter(f, operator, value).pluck(:entity_id) end.uniq query.where(id: union_ids) end else fields_by_name = HasTypedEAV.definitions_by_name(defs) filters.inject(all) do |query, filter| filter = filter.to_h.with_indifferent_access name = filter[:n] || filter[:name] operator = (filter[:op] || filter[:operator] || :eq).to_sym value = filter.key?(:v) ? filter[:v] : filter[:value] field = fields_by_name[name.to_s] unless field raise ArgumentError, "Unknown typed field '#{name}' for #{self.name}. " \ "Available fields: #{fields_by_name.keys.join(", ")}" end matching_ids = TypedEAV::QueryBuilder.entity_ids(field, operator, value) query.where(id: matching_ids) end end end |
#with_field(name, operator_or_value = nil, value = nil, scope: UNSET_SCOPE) ⇒ Object
Shorthand for single-field queries.
Contact.with_field("age", :gt, 21)
Contact.with_field("active", true) # op defaults to :eq
Contact.with_field("name", :contains, "smith")
215 216 217 218 219 220 221 222 |
# File 'lib/typed_eav/has_typed_eav.rb', line 215 def with_field(name, operator_or_value = nil, value = nil, scope: UNSET_SCOPE) if value.nil? && !operator_or_value.is_a?(Symbol) # Two-arg form: with_field("name", "value") implies :eq where_typed_eav({ name: name, op: :eq, value: operator_or_value }, scope: scope) else where_typed_eav({ name: name, op: operator_or_value, value: value }, scope: scope) end end |