Skuld.Query.Contract (skuld v0.3.0)

View Source

Macro for defining typed batchable fetch contracts with deffetch declarations.

A Query contract defines a set of fetch operations, generating:

  • Operation structs — nested struct modules per fetch (e.g., MyContract.GetUser)
  • Caller functions — typed public API returning computation(return_type), which suspend the current fiber for batched execution
  • Executor behaviour (__MODULE__.Executor) — typed callbacks for batch execution, one per fetch
  • Dispatch function__dispatch__/3 routes from batch key to executor callback
  • Wiring functionwith_executor/2 installs an executor module for all fetches
  • Bang variants — unwrap {:ok, v} or dispatch Throw (when applicable)
  • Introspection__query_operations__/0

Example

defmodule MyApp.Queries.Users do
  use Skuld.Query.Contract

  deffetch get_user(id :: String.t()) :: User.t() | nil
  deffetch get_users_by_org(org_id :: String.t()) :: [User.t()]
  deffetch get_user_count(org_id :: String.t()) :: non_neg_integer()
end

This generates:

# Operation struct
MyApp.Queries.Users.GetUser   # defstruct [:id]
MyApp.Queries.Users.GetUsersByOrg   # defstruct [:org_id]

# Executor behaviour
MyApp.Queries.Users.Executor
@callback get_user(ops :: [{reference(), GetUser.t()}]) :: computation(...)

# Caller (suspends fiber for batching)
@spec get_user(String.t()) :: Types.computation(User.t() | nil)
def get_user(id)

# Wiring
@spec with_executor(computation(), module()) :: computation()
def with_executor(comp, executor_module)

Executor Implementation

defmodule MyApp.Queries.Users.EctoExecutor do
  @behaviour MyApp.Queries.Users.Executor

  @impl true
  def get_user(ops) do
    ids = Enum.map(ops, fn {_ref, %GetUser{id: id}} -> id end) |> Enum.uniq()
    Comp.bind(Reader.ask(:repo), fn repo ->
      results = repo.all(User, ids)
      by_id = Map.new(results, &{&1.id, &1})
      Comp.pure(Map.new(ops, fn {ref, %GetUser{id: id}} -> {ref, Map.get(by_id, id)} end))
    end)
  end
end

Wiring

my_comp
|> MyApp.Queries.Users.with_executor(MyApp.Queries.Users.EctoExecutor)
|> FiberPool.with_handler()
|> Comp.run()

Bulk Wiring

my_comp
|> Skuld.Query.Contract.with_executors([
  {MyApp.Queries.Users, MyApp.Queries.Users.EctoExecutor},
  {MyApp.Queries.Orders, MyApp.Queries.Orders.EctoExecutor}
])

Bang Variant Generation

Same rules as Port.Contract:

  • Auto-detect (default): If the return type contains {:ok, T}, a bang variant is generated that unwraps {:ok, value} or dispatches Throw on {:error, reason}.
  • bang: true: Force standard unwrapping.
  • bang: false: Suppress bang generation.
  • bang: unwrap_fn: Custom unwrap function.

Summary

Functions

Define a typed fetch operation.

Deprecated: use deffetch instead.

Install multiple contract/executor pairs in one call.

Functions

deffetch(spec, opts \\ [])

(macro)

Define a typed fetch operation.

Syntax

deffetch function_name(param :: type(), ...) :: return_type()
deffetch function_name(param :: type(), ...) :: return_type(), bang: option

Each deffetch declaration generates an operation struct, caller function, executor callback, dispatch clause, and optionally a bang variant.

The bang option follows the same rules as Port.Contract.defport:

  • omitted -- auto-detect: generate bang only if return type contains {:ok, T}
  • true -- force standard {:ok, v} / {:error, r} unwrapping
  • false -- suppress bang generation
  • unwrap_fn -- custom unwrap function

defquery(spec, opts \\ [])

(macro)

Deprecated: use deffetch instead.

defquery is a deprecated alias for deffetch. It will be removed in a future release.

with_executors(comp, pairs)

@spec with_executors(
  Skuld.Comp.Types.computation(),
  [{module(), module()}] | %{required(module()) => module()}
) :: Skuld.Comp.Types.computation()

Install multiple contract/executor pairs in one call.

Accepts either a list of {contract_module, executor_module} tuples or a map.

Examples

comp
|> Skuld.Query.Contract.with_executors([
  {MyApp.Queries.Users, MyApp.Queries.Users.EctoExecutor},
  {MyApp.Queries.Orders, MyApp.Queries.Orders.EctoExecutor}
])

comp
|> Skuld.Query.Contract.with_executors(%{
  MyApp.Queries.Users => MyApp.Queries.Users.EctoExecutor,
  MyApp.Queries.Orders => MyApp.Queries.Orders.EctoExecutor
})