Skuld.Effects.Query (skuld v0.1.13)
View SourceBackend-agnostic data query effect.
This effect lets domain code express "run this query" without binding to a particular storage layer. Each request specifies:
mod– module implementing the queryname– function name insidemodparams– map/struct of query parameters
Result Tuple Convention
Query handlers should return {:ok, value} or {:error, reason} tuples.
This convention enables two request modes:
request/3– returns the result tuple as-is for caller to handlerequest!/3– unwraps{:ok, value}or dispatchesThrowon error
Example
alias Skuld.Effects.Query
# Query implementation returns result tuples
defmodule MyApp.UserQueries do
def find_by_id(%{id: id}) do
case Repo.get(User, id) do
nil -> {:error, {:not_found, User, id}}
user -> {:ok, user}
end
end
end
# Using request/3 - returns result tuple
defcomp find_user(id) do
result <- Query.request(MyApp.UserQueries, :find_by_id, %{id: id})
case result do
{:ok, user} -> return(user)
{:error, _} -> return(nil)
end
end
# Using request!/3 - unwraps or throws
defcomp find_user!(id) do
user <- Query.request!(MyApp.UserQueries, :find_by_id, %{id: id})
return(user)
end
# Runtime: dispatch to actual query modules
find_user!(123)
|> Query.with_handler(%{MyApp.UserQueries => :direct})
|> Throw.with_handler()
|> Comp.run!()
# Test: stub responses (use result tuples)
find_user!(123)
|> Query.with_test_handler(%{
Query.key(MyApp.UserQueries, :find_by_id, %{id: 123}) => {:ok, %{id: 123, name: "Alice"}}
})
|> Throw.with_handler()
|> Comp.run!()
Summary
Types
Opaque parameter payload
Query module implementing name/1
Function exported by query_module
Registry mapping query modules to resolvers
Registry entry for dispatching queries.
Functions
Build a canonical key usable with with_test_handler/2.
Build a query request for the given module/function.
Build a query request that unwraps the result or throws on error.
Install a scoped Query handler for a computation.
Install a test handler with canned responses.
Types
Opaque parameter payload
@type query_module() :: module()
Query module implementing name/1
@type query_name() :: atom()
Function exported by query_module
@type registry() :: %{required(query_module()) => resolver()}
Registry mapping query modules to resolvers
@type resolver() :: :direct | (query_module(), query_name(), params() -> term()) | {module(), atom()} | module()
Registry entry for dispatching queries.
:direct– callapply(mod, name, [params])function(arity 3) –fun.(mod, name, params){module, function}– invokesapply(module, function, [mod, name, params])module– invokesmodule.handle_query(mod, name, params)
Functions
@spec key(query_module(), query_name(), params()) :: {query_module(), query_name(), binary()}
Build a canonical key usable with with_test_handler/2.
Parameters are normalized so that structurally-equal maps/structs produce the same key, independent of key ordering.
Example
Query.key(MyApp.UserQueries, :find_by_id, %{id: 123})
@spec request(query_module(), query_name(), params()) :: Skuld.Comp.Types.computation()
Build a query request for the given module/function.
Returns the result tuple {:ok, value} or {:error, reason} as-is,
allowing the caller to handle errors explicitly.
Example
Query.request(MyApp.UserQueries, :find_by_id, %{id: 123})
# => {:ok, %User{...}} or {:error, {:not_found, User, 123}}
@spec request!(query_module(), query_name(), params()) :: Skuld.Comp.Types.computation()
Build a query request that unwraps the result or throws on error.
Expects the query handler to return {:ok, value} or {:error, reason}.
On success, returns the unwrapped value. On error, dispatches a
Skuld.Effects.Throw effect with the reason.
Requires a Throw.with_handler/1 in the handler chain.
Example
Query.request!(MyApp.UserQueries, :find_by_id, %{id: 123})
# => %User{...} or throws {:not_found, User, 123}
@spec with_handler(Skuld.Comp.Types.computation(), registry(), keyword()) :: Skuld.Comp.Types.computation()
Install a scoped Query handler for a computation.
Pass a registry map keyed by query module to control how queries are dispatched. Each entry can be one of:
:direct– callapply(mod, name, [params])function(arity 3) –fun.(mod, name, params){module, function}– invokesapply(module, function, [mod, name, params])module– invokesmodule.handle_query(mod, name, params)
Handlers may return any value. To signal errors, raise or use
Skuld.Effects.Throw.throw/1.
Example
my_comp
|> Query.with_handler(%{
MyApp.UserQueries => :direct,
MyApp.OrderQueries => MyApp.CachedQueryHandler
})
|> Comp.run!()
@spec with_test_handler(Skuld.Comp.Types.computation(), map(), keyword()) :: Skuld.Comp.Types.computation()
Install a test handler with canned responses.
Provide a map of responses keyed by Query.key/3. Missing keys will
throw {:query_not_stubbed, key}.
Example
responses = %{
Query.key(MyApp.UserQueries, :find_by_id, %{id: 123}) => %{id: 123, name: "Alice"},
Query.key(MyApp.UserQueries, :find_by_id, %{id: 456}) => nil
}
my_comp
|> Query.with_test_handler(responses)
|> Throw.with_handler()
|> Comp.run!()