The built-in Flicker.Provider that Tier 1 declarative component config
compiles to (ADR-001) — searching and fetching directly off an Ash
resource, with no provider module of your own to write.
This module only compiles when ash is present (guarded by
Code.ensure_loaded?/1, ADR-006): it's inert, not merely undocumented,
in a build without ash in its deps.
Every emitted Flicker.Result carries the underlying record as
meta.record — the documented, stable way for an :option slot to
render icons, badges, or anything else the two label strings can't
express.
Configure it with {Flicker.Providers.AshResource, opts}:
:resource— required. The Ash resource module to read.:search— required list of attribute namessearch/2'silike-style (case-insensitive substring) match runs over.:option_label— required. An attribute name, or afun(record) :: String.t(), producing each result's:label.:option_sublabel— optional. An attribute name, or afun(record) :: String.t() | nil, producing each result's:sublabel.:read_action— the read action to run. Defaults to:read.:read_action_args— arguments for:read_action;:queryis replaced with the typed search text.:fetch_action— read action used byfetch/2. Defaults to:read.:load— loads applied to both searches and selected-value fetches.:limit— max results forsearch/2. Defaults to25.:sort— sort applied tosearch/2's query (anyAsh.Query.sort/2input). Optional.:filter— a base filter (anyAsh.Query.filter_input/2input) applied before the search-text match. Optional.:facets— optional list of facet keys forfacets/1to expand (Spec 003), and the registrysearch/2composesFlicker.Query.to_filter/2against to scope candidates byquery.facetsbefore the free-text match runs. Each entry is either a bare atom (:status) naming an attribute, relationship, aggregate, or calculation to derive aFlicker.Facet.t()from, a{key, overrides}pair (status: [type: :enum]) whereoverrides— any of:type,:path,:attribute,:aggregate,:op,:label— takes precedence over what introspection would derive, or an already-builtFlicker.Facet.t()(passed through unchanged — howFlicker.select/1'sfacets:attr wires this in, since it resolves toFlicker.Facetstructs before reaching the provider).
:actor, :tenant, and :limit also arrive per-call, merged in by
Flicker.Provider.run_search/3 / run_fetch/3 — reads are always
actor/tenant-scoped (ADR-004): this provider passes them straight
through to Ash.Query.for_read/4 and Ash.read/2, and never bypasses
policies.
fetch/2 resolves all of values in a single primary_key in ^values
read (ADR-003) — one query, not one per value — and silently omits
values that don't resolve (deleted records, or records a policy now
hides): this is a normal partial result, not an error.
Composite primary keys aren't supported yet — fetch/2 assumes a
single-attribute primary key.
Summary
Functions
Counts how many records each requested facet's values would match (Spec 021).
Expands opts[:facets] into [Flicker.Facet.t()] by introspecting
opts[:resource] — the facet registry
(Spec 003).
Resolves values against opts[:resource]'s primary key in a single
primary_key in ^values read (ADR-003). Values that don't resolve are
simply absent from the result — not an error.
Runs opts[:read_action] on opts[:resource], filtering by
opts[:filter] (if any), then query.facets (Spec 003 — distinct facet
keys AND, repeated instances of the same key OR, composed via
Flicker.Query.to_filter/2 against this provider's own facet registry
so the candidates are scoped before the text match runs), then
query.text (case-insensitive substring match over opts[:search]),
sorted by opts[:sort] (if any), limited to opts[:limit] and offset
by opts[:offset] (default 0, via Ash.Query.offset/2 —
Spec 010).
Always actor/tenant-scoped (ADR-004).
Functions
@spec facet_counts(Flicker.Query.t(), [Flicker.Facet.t()], keyword()) :: {:ok, %{required(atom()) => %{required(term()) => non_neg_integer()}}} | {:error, term()}
Counts how many records each requested facet's values would match (Spec 021).
Drill-down semantics (ADR-014):
for each counted facet, every other active facet and the free text apply,
but that facet's own clauses do not. Counting against the whole query
instead would make every sibling value read 0 the instant one is
selected — the user's own choice erasing the information they needed.
Every read is actor:/tenant:-scoped exactly as search/2's is, so a
count can never include a record the actor couldn't have read.
An :enum facet is counted across all its declared values, zeroes
included, because a genuine zero and an uncounted value are different facts
and only the full set lets an editor show Archived 0 rather than dropping
the row. Other counted facets report only the values actually present,
capped by :count_limit (default 50) — enumerating every related record to
discover its zeroes is unbounded work.
@spec facets(keyword()) :: [Flicker.Facet.t()]
Expands opts[:facets] into [Flicker.Facet.t()] by introspecting
opts[:resource] — the facet registry
(Spec 003).
Each entry in opts[:facets] is a bare atom (:status) naming an
attribute, relationship, aggregate, or calculation on the resource, or
a {key, overrides} pair layering :type, :path, :attribute,
:aggregate, :op, or :label on top of what introspection would
derive. Per the spec's type table:
- an
Ash.Type.Enum(or a plain attribute with aone_ofconstraint) derivestype: :enum,:eq-only,:values, and:value_labels(the enum's own labels, or a humanised fallback); a value outside:valuesis rejected by the parser (degrades to free text) rather than reaching a filter. :booleanderivestype: :boolean,:eq-only.- a date/datetime attribute derives
type: :date, operators:eq:gte:lt(relative values like7dare the parser's concern — seeFlicker.Query). :integer/:decimal/:floatderive numerictype, the full comparison operator set.- a
belongs_to/has_*relationship derives a:string-typed facet (the related record's id) with:relatedset to%{resource: destination}— the nested-search descriptor a recursive Flicker search would run over — and:targetresolved to the relationship's own filter path (the source attribute forbelongs_to,[name, primary_key]forhas_*). - an aggregate or calculation derives from its own resolved Ash type the same way an attribute would (numeric, boolean, ...).
- anything else (plain
:string, or a field introspection can't find) derivestype: :string,:contains-only — freeilike, no picklist.
overrides[:path] (a relationship path ending in an attribute, e.g.
[:worker, :full_name]) and overrides[:attribute] /
overrides[:aggregate] (an explicit field name, when it differs from
the facet key) redirect which field is introspected;
overrides[:type] and overrides[:op] replace the derived :type /
:default_op outright.
@spec fetch([term()], keyword()) :: {:ok, [Flicker.Result.t()]} | {:error, term()}
Resolves values against opts[:resource]'s primary key in a single
primary_key in ^values read (ADR-003). Values that don't resolve are
simply absent from the result — not an error.
@spec search(Flicker.Query.t(), keyword()) :: {:ok, [Flicker.Result.t()]} | {:error, term()}
Runs opts[:read_action] on opts[:resource], filtering by
opts[:filter] (if any), then query.facets (Spec 003 — distinct facet
keys AND, repeated instances of the same key OR, composed via
Flicker.Query.to_filter/2 against this provider's own facet registry
so the candidates are scoped before the text match runs), then
query.text (case-insensitive substring match over opts[:search]),
sorted by opts[:sort] (if any), limited to opts[:limit] and offset
by opts[:offset] (default 0, via Ash.Query.offset/2 —
Spec 010).
Always actor/tenant-scoped (ADR-004).