AshDyan — runtime-driven dynamic analysis for any Ash resource.
AshDyan is a standalone Ash extension (no dependency on ash_phoenix_gen_api)
that lets any Ash resource/domain be declared "analyzable", and exposes a
runtime function/API where a caller sends a request spec and gets back
chart-ready aggregated data.
Positioning
- Turns "give me a chart of X grouped by Y, filtered by Z" into a generic, safe, reusable runtime capability across any Ash resource — instead of writing a bespoke aggregate action per chart.
- It is not a full BI/reporting engine, not a query builder UI, and not tied to Phoenix/Channels. Delivery (HTTP controller, Channel, LiveView, gen_api mfa) is a thin adapter on top.
Security model
The dyan DSL is a whitelist. A runtime request can only reference fields,
functions, buckets, and filter targets declared there — this is what makes
"arbitrary column + arbitrary filter from the client" safe rather than an
injection/DoS vector.
Entry point
AshDyan.run/1 (or AshDyan.run/2 with an actor for policy checks) is the
single entry point. It validates the spec, builds an Ash.Query, runs it
through the resource's normal read action (so Ash policies/authorization still
apply), and formats the result.
Logging
AshDyan.run/2 emits structured logs via the Logger standard library:
a :debug line when a request starts, a :debug line when a request is
rejected during validation/configuration, a :warning when the requested
analysis type is unsupported by the resource's data layer, and an :error
line when the underlying read fails. Filter contents are never logged.
See AshDyan.Request for the request spec shape and AshDyan.Info for
introspection helpers.
Summary
Types
Numeric aggregate functions.
Analysis capabilities exposed by AshDyan.
Time bucket granularities.
Types
@type aggregate_function() ::
:sum
| :avg
| :min
| :max
| :count
| :count_distinct
| :stddev
| :variance
| :median
Numeric aggregate functions.
@type capability() ::
:frequency | :aggregate | :time_bucket | :percentile | :histogram
Analysis capabilities exposed by AshDyan.
@type run_opt() :: {:actor, term()} | {:tenant, term()} | {:timeout, pos_integer() | :infinity} | {:data, [Ash.Resource.Record.t()]}
Options for run/2.
:actor— actor passed to the read action for policy checks.:tenant— tenant for multitenant resources.:timeout— overrides the per-request query timeout (defaults to the resource'squery_timeout, which is always enforced).:data— explicit in-memory dataset for theAsh.DataLayer.Simplelayer (used by tests and embedded resources).
@type time_bucket() :: :minute | :hour | :day | :week | :month | :quarter | :year
Time bucket granularities.
Functions
@spec run(AshDyan.Request.t() | map(), [run_opt()]) :: {:ok, AshDyan.Result.t()} | {:error, term()}
Run a dynamic analysis request.
Options
:actor— the actor to authorize as (passed to the read action).:tenant— the tenant for multitenant resources.:timeout— overrides the per-request query timeout (defaults to the resource's configuredquery_timeout, which is always enforced).:data— an explicit in-memory dataset (Ash.DataLayer.Simpleonly). When supplied, the query reads from this list instead of the data layer. Mostly used by tests and embedded resources.
Returns {:ok, %AshDyan.Result{}} or {:error, error}. Validation and
configuration errors are returned as {:error, %AshDyan.Error{}} naming the
offending field; read failures are returned as the underlying Ash error.
@spec run!(AshDyan.Request.t() | map(), [run_opt()]) :: AshDyan.Result.t()
Same as run/2 but raises on error.
@spec supports?(module(), capability()) :: boolean()
Returns true if the resource's data layer supports the given capability.
Capabilities: :frequency, :aggregate, :time_bucket, :percentile,
:histogram.
This is surfaced explicitly so callers can discover data-layer limits before issuing a query, rather than discovering them at query time.