defmodule Skuld.QueryContract do @moduledoc """ Primary API for query contracts, wiring, and caching. ## Defining a query contract defmodule MyApp.Queries do use Skuld.QueryContract deffetch get_user(id :: String.t()) :: User.t() | nil deffetch get_orders(user_id :: String.t()) :: [Order.t()] end ## Wiring executors comp |> Skuld.QueryContract.with_executor(MyApp.Queries, MyApp.Queries.EctoExecutor) |> FiberPool.with_handler() |> Comp.run() ## With caching comp |> Skuld.QueryContract.with_cached_executor(MyApp.Queries, MyApp.Queries.EctoExecutor) |> FiberPool.with_handler() |> Comp.run() """ alias Skuld.Comp alias Skuld.Comp.InternalSuspend alias Skuld.Effects.FiberPool alias Skuld.Effects.FreshInt @doc false defmacro __using__(opts) do quote do use Skuld.Query.Contract, unquote(opts) end end @doc false @spec batchable_op({module(), atom()}, term()) :: Comp.Types.computation() def batchable_op(batch_key, op) do fn env, k -> resume = fn result, resume_env -> k.(result, resume_env) end {request_id, env} = Comp.call(FreshInt.fresh_integer(), env, &Comp.identity_k/2) suspend = InternalSuspend.batch(batch_key, op, request_id, resume) {suspend, env} end end @doc """ Install an executor module for a single query contract. The executor module must implement the contract's callbacks. """ @spec with_executor(Comp.Types.computation(), module(), module()) :: Comp.Types.computation() def with_executor(comp, contract_module, executor_module) do Skuld.Query.Contract.with_executors(comp, [{contract_module, executor_module}]) end @doc """ Install executor modules for multiple query contracts. Accepts either a list of `{contract_module, executor_module}` tuples or a map. """ defdelegate with_executors(comp, pairs), to: Skuld.Query.Contract @doc """ Install a caching-wrapped executor for a single contract. Shorthand for `with_cached_executors/2`. """ defdelegate with_cached_executor(comp, contract_module, executor_module), to: Skuld.Query.Cache @doc """ Install caching-wrapped executors for multiple contracts. Initialises a scoped cache and wraps executors so identical queries return cached results without re-executing. """ defdelegate with_cached_executors(comp, pairs), to: Skuld.Query.Cache @doc """ Map a function over items, running each result as a fiber so that `deffetch` calls within each iteration batch together. Requires `FiberPool.with_handler` in the stack. Results are returned in the same order as the input items. ## Example details <- Query.map(order_ids, &AccountQueries.fetch_order_details/1) """ @spec map([term()], (term() -> Comp.Types.computation())) :: Comp.Types.computation() defdelegate map(items, fun), to: FiberPool end