Skuld.Effects.Query (skuld v0.1.13)

View Source

Backend-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 query
  • name – function name inside mod
  • params – 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 handle
  • request!/3 – unwraps {:ok, value} or dispatches Throw on 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

params()

@type params() :: map() | struct()

Opaque parameter payload

query_module()

@type query_module() :: module()

Query module implementing name/1

query_name()

@type query_name() :: atom()

Function exported by query_module

registry()

@type registry() :: %{required(query_module()) => resolver()}

Registry mapping query modules to resolvers

resolver()

@type resolver() ::
  :direct
  | (query_module(), query_name(), params() -> term())
  | {module(), atom()}
  | module()

Registry entry for dispatching queries.

  • :direct – call apply(mod, name, [params])
  • function (arity 3) – fun.(mod, name, params)
  • {module, function} – invokes apply(module, function, [mod, name, params])
  • module – invokes module.handle_query(mod, name, params)

Functions

key(mod, name, params)

@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})

request(mod, name, params \\ %{})

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}}

request!(mod, name, params \\ %{})

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}

with_handler(comp, registry \\ %{}, opts \\ [])

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 – call apply(mod, name, [params])
  • function (arity 3) – fun.(mod, name, params)
  • {module, function} – invokes apply(module, function, [mod, name, params])
  • module – invokes module.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!()

with_test_handler(comp, responses, opts \\ [])

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!()