# `Flicker.Providers.AshResource`
[🔗](https://github.com/team-alembic/flicker/blob/v0.1.0/lib/flicker/providers/ash_resource.ex#L2)

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 names `search/2`'s `ilike`-style
    (case-insensitive substring) match runs over.
  * `:option_label` — required. An attribute name, or a
    `fun(record) :: String.t()`, producing each result's `:label`.
  * `:option_sublabel` — optional. An attribute name, or a
    `fun(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`; `:query` is
    replaced with the typed search text.
  * `:fetch_action` — read action used by `fetch/2`. Defaults to `:read`.
  * `:load` — loads applied to both searches and selected-value fetches.
  * `:limit` — max results for `search/2`. Defaults to `25`.
  * `:sort` — sort applied to `search/2`'s query (any `Ash.Query.sort/2`
    input). Optional.
  * `:filter` — a base filter (any `Ash.Query.filter_input/2` input)
    applied before the search-text match. Optional.
  * `:facets` — optional list of facet keys for `facets/1` to expand
    ([Spec 003](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-003-faceted-search.md)),
    and the registry `search/2` composes `Flicker.Query.to_filter/2`
    against to scope candidates by `query.facets` before the free-text
    match runs. Each entry is either a bare atom (`:status`) naming an
    attribute, relationship, aggregate, or calculation to derive a
    `Flicker.Facet.t()` from, a `{key, overrides}` pair (`status:
    [type: :enum]`) where `overrides` — any of `:type`, `:path`,
    `:attribute`, `:aggregate`, `:op`, `:label` — takes precedence over
    what introspection would derive, or an already-built `Flicker.Facet.t()`
    (passed through unchanged — how `Flicker.select/1`'s `facets:` attr
    wires this in, since it resolves to `Flicker.Facet` structs 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.

# `facet_counts`

```elixir
@spec facet_counts(Flicker.Query.t(), [Flicker.Facet.t()], keyword()) ::
  {:ok, %{required(atom()) =&gt; %{required(term()) =&gt; non_neg_integer()}}}
  | {:error, term()}
```

Counts how many records each requested facet's values would match
([Spec 021](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-021-facet-value-counts.md)).

**Drill-down semantics** ([ADR-014](https://github.com/team-alembic/flicker/blob/main/docs/adrs/adr-014-facet-counts-are-provider-computed-and-actor-scoped.md)):
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.

# `facets`

```elixir
@spec facets(keyword()) :: [Flicker.Facet.t()]
```

Expands `opts[:facets]` into `[Flicker.Facet.t()]` by introspecting
`opts[:resource]` — the facet registry
([Spec 003](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-003-faceted-search.md)).

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 a `one_of`
    constraint) derives `type: :enum`, `:eq`-only, `:values`, and
    `:value_labels` (the enum's own labels, or a humanised fallback);
    a value outside `:values` is rejected by the parser (degrades to
    free text) rather than reaching a filter.
  * `:boolean` derives `type: :boolean`, `:eq`-only.
  * a date/datetime attribute derives `type: :date`, operators `:eq`
    `:gte` `:lt` (relative values like `7d` are the parser's concern —
    see `Flicker.Query`).
  * `:integer`/`:decimal`/`:float` derive numeric `type`, the full
    comparison operator set.
  * a `belongs_to`/`has_*` relationship derives a `:string`-typed
    facet (the related record's id) with `:related` set to
    `%{resource: destination}` — the nested-search descriptor a
    recursive Flicker search would run over — and `:target` resolved
    to the relationship's own filter path (the source attribute for
    `belongs_to`, `[name, primary_key]` for `has_*`).
  * 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) derives `type: :string`, `:contains`-only — free `ilike`,
    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.

# `fetch`

```elixir
@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.

# `search`

```elixir
@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](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-010-windowed-search.md)).
Always actor/tenant-scoped (ADR-004).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
