diff --git a/lib/dx.ex b/lib/dx.ex index aed857ea..a1a62771 100644 --- a/lib/dx.ex +++ b/lib/dx.ex @@ -12,6 +12,8 @@ defmodule Dx do The corresponding `get!/3`, `load!/3` and `put!/3` functions return `result` directly, or otherwise raise an exception. + If you want to apply default options to all functions, `use` `Dx.Repo`. + Arguments: - **subjects** can either be an individual subject (with the given predicates defined on it), or a list of subjects. Passing an individual subject will return the predicates for the subject, passing a list will return a list of them. diff --git a/lib/dx/repo.ex b/lib/dx/repo.ex new file mode 100644 index 00000000..b3f2a48c --- /dev/null +++ b/lib/dx/repo.ex @@ -0,0 +1,141 @@ +defmodule Dx.Repo do + @moduledoc """ + Defines a repository with default options, similar to `Ecto.Repo`. + + When used, the repository expects the `:otp_app` as option. + The `:otp_app` should point to an OTP application that has + the repository configuration. For example, the repository: + + defmodule Repo do + use Dx.Repo, + otp_app: :my_app, + loader: Dx.Loaders.Dataloader, + loader_options: [telemetry_options: [dx: true]] + end + + Could be configured with: + + config :my_app, Repo, + loader_options: [timeout: 20_000] + + See `Dx` for further options. The options are deep-merged, + with the following order of precedence: + + 1. Options passed to boundary functions, such as `Dx.get/3` + 2. Options returned by `Dx.Repo.default_options/1` callback + 3. Options from config + 4. Options from `use Dx.Repo, ...`. + """ + + @doc false + defmacro __using__(opts) do + quote bind_quoted: [opts: opts] do + @behaviour Dx.Repo + @otp_app Keyword.fetch!(opts, :otp_app) + @opts opts + + def default_options(_operation), do: [] + defoverridable default_options: 1 + + @compile {:inline, prepare_opts: 2} + defp prepare_opts(operation_name, opts) do + config = Application.get_env(otp_app, __MODULE__, []) + + @opts + |> Dx.Util.Keyword.deep_merge(config) + |> Dx.Util.Keyword.deep_merge(default_options(operation_name)) + |> Dx.Util.Keyword.deep_merge(opts) + end + + def get(records, predicates, opts \\ []) do + Dx.get(records, predicates, prepare_opts(opts, :get)) + end + + def get!(records, predicates, opts \\ []) do + Dx.get!(records, predicates, prepare_opts(opts, :get)) + end + + def load(records, predicates, opts \\ []) do + Dx.load(records, predicates, prepare_opts(opts, :load)) + end + + def load!(records, predicates, opts \\ []) do + Dx.load!(records, predicates, prepare_opts(opts, :load)) + end + + def put(records, predicates, opts \\ []) do + Dx.put(records, predicates, prepare_opts(opts, :put)) + end + + def put!(records, predicates, opts \\ []) do + Dx.put!(records, predicates, prepare_opts(opts, :put)) + end + + def filter(records, condition, opts \\ []) when is_list(records) do + Dx.filter(records, condition, prepare_opts(opts, :filter)) + end + + def reject(records, condition, opts \\ []) when is_list(records) do + Dx.reject(records, condition, prepare_opts(opts, :reject)) + end + + def query_all(queryable, condition, opts \\ []) do + Dx.query_all(queryable, condition, prepare_opts(opts, :query_all)) + end + + def query_one(queryable, condition, opts \\ []) do + Dx.query_one(queryable, condition, prepare_opts(opts, :query_one)) + end + end + end + + ## User callbacks + + @doc """ + A user customizable callback invoked to retrieve default options + for operations. + This can be used to provide default values per operation that + have higher precedence than the values given on configuration. + """ + @doc group: "User callbacks" + @callback default_options(operation) :: Keyword.t() + when operation: :get | :load | :put | :filter | :reject | :query_one | :query_all + + ## Query API + + @type record :: any() + @type predicate :: any() + @type condition :: any() + @type queryable :: any() + @type opts :: Keyword.t() + + @doc group: "Query API" + @callback get([record], [predicate], opts) :: any() + + @doc group: "Query API" + @callback get!([record], [predicate], opts) :: any() + + @doc group: "Query API" + @callback load([record], [predicate], opts) :: any() + + @doc group: "Query API" + @callback load!([record], [predicate], opts) :: any() + + @doc group: "Query API" + @callback put([record], [predicate], opts) :: any() + + @doc group: "Query API" + @callback put!([record], [predicate], opts) :: any() + + @doc group: "Query API" + @callback filter([record], condition, opts) :: any() + + @doc group: "Query API" + @callback reject([record], condition, opts) :: any() + + @doc group: "Query API" + @callback query_all(queryable, condition, opts) :: any() + + @doc group: "Query API" + @callback query_one(queryable, condition, opts) :: any() +end diff --git a/lib/dx/util/keyword.ex b/lib/dx/util/keyword.ex new file mode 100644 index 00000000..277a3eab --- /dev/null +++ b/lib/dx/util/keyword.ex @@ -0,0 +1,18 @@ +defmodule Dx.Util.Keyword do + def deep_merge(left, right) do + Keyword.merge(left, right, &deep_resolve/3) + end + + # Key exists in both lists, and both values are lists as well. + # These can be merged recursively. + defp deep_resolve(_key, left = [{_, _} | _], right = [{_, _} | _]) do + deep_merge(left, right) + end + + # Key exists in both maps, but at least one of the values is + # NOT a list. We fall back to standard merge behavior, preferring + # the value on the right. + defp deep_resolve(_key, _left, right) do + right + end +end