This guide covers how to execute read queries and direct mutation actions using Alva SDK composables.
1. Overview & Conventions
Alva provides two distinct interaction patterns for reading and mutating data:
- Reactive Queries (
use_<action>_query): Reactive data fetching composable for Ash:readactions. Automatically re-fetches when input reactive getters change, with built-in debouncing. - Direct Actions (
alva.<domain>.<action>): One-off async command execution for buttons, background tasks, or imperative workflow calls.
2. Ash Backend Definition
Define your read action with filtering arguments inside AlvaDemo.Catalog.Product:
# lib/alva_demo/catalog/product.ex
defmodule AlvaDemo.Catalog.Product do
require Ash.Query
use Ash.Resource,
domain: AlvaDemo.Catalog,
data_layer: Ash.DataLayer.Ets,
extensions: [Alva.Resource]
alva do
event(:catalog_list_products, name: "catalog.list_products", action: :list)
event(:catalog_adjust_stock, name: "catalog.adjust_stock", action: :adjust_stock)
end
actions do
defaults([:destroy])
read :list do
public?(true)
argument(:query, :string, allow_nil?: true)
argument(:max_stock, :integer, allow_nil?: true)
prepare(fn query, _context ->
query
|> Ash.Query.sort(name: :asc)
|> filter_product_query(Ash.Query.get_argument(query, :query))
|> filter_max_stock(Ash.Query.get_argument(query, :max_stock))
end)
end
update :adjust_stock do
public?(true)
accept([:stock])
end
end
defp filter_product_query(query, nil), do: query
defp filter_product_query(query, search_term) when is_binary(search_term) do
case String.trim(search_term) do
"" -> query
pattern ->
require Ash.Expr
Ash.Query.filter(query, Ash.Expr.expr(contains(name, ^pattern) or contains(description, ^pattern)))
end
end
defp filter_max_stock(query, nil), do: query
defp filter_max_stock(query, max_stock) do
require Ash.Expr
Ash.Query.filter(query, Ash.Expr.expr(stock <= ^max_stock))
end
end3. LiveView Integration
Query events and direct actions are routed over WebSockets via Alva.Dispatcher without needing manual handle_event/3 functions:
# lib/alva_demo_web/live/customer_storefront_live.ex
defmodule AlvaDemoWeb.CustomerStorefrontLive do
use AlvaDemoWeb, :live_view
use Alva.LiveView
end4. Frontend TypeScript Library Usage
4.1 Reactive Search Query (alva.catalog.use_list_products_query)
<script setup lang="ts">
import { ref } from "vue";
import { useAlva } from "@/js/alva";
const alva = useAlva();
const searchQuery = ref("");
// Re-fetches automatically when `searchQuery` changes
const { data, loading, error, refetch } = alva.catalog.use_list_products_query(
() => ({ query: searchQuery.value }),
{ debounceMs: 300 }
);
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="Search..." />
<div v-if="loading">Searching...</div>
<div v-else-if="error" class="text-red-600">Query failed: {{ error.message }}</div>
<ul v-else-if="data">
<li v-for="product in data" :key="product.id">{{ product.name }}</li>
</ul>
</div>
</template>4.2 Direct Imperative Action (alva.sales.begin_processing)
<script setup lang="ts">
import { ref } from "vue";
import { useAlva } from "@/js/alva";
const alva = useAlva();
const isProcessing = ref(false);
const errorMessage = ref<string | null>(null);
async function handleBeginProcessing(orderId: string) {
isProcessing.value = true;
errorMessage.value = null;
const result = await alva.sales.begin_processing({ id: orderId });
isProcessing.value = false;
if (result.ok) {
console.log("Order processing started!");
} else {
errorMessage.value = result.error?.message || "Failed to update order status.";
}
}
</script>