Packages

phoenix_kit

1.7.201
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit install child_order.ex
Raw

lib/phoenix_kit/install/child_order.ex

defmodule PhoenixKit.Install.ChildOrder do
@moduledoc """
Reads a host application's `application.ex` and checks that the Ecto Repo
starts BEFORE `PhoenixKit.Supervisor` and `Oban` in the supervision
`children` list.
`PhoenixKit.Supervisor` reads Settings / OAuth configuration from the
database as it boots, and Oban opens a connection pool against the same
Repo — so both MUST appear after the Repo in the children list. A
mis-ordered list crashes the app at startup (typically an Oban crash-loop:
the pool has no database to reach yet).
`mix phoenix_kit.install` positions its child after the detected Repo, but a
hand-edited `application.ex` — or an Igniter anchor miss that prepends the
child instead of inserting it after the Repo — can regress the order. This
module is the deterministic safety net: `mix phoenix_kit.doctor` runs
`check/2` against the host's source so both fresh and existing installs are
caught, independent of how the children ended up ordered.
Everything here is a pure function over source text — no application boot,
no database — so it is unit-testable in isolation.
"""
# Children that depend on the Repo already being started.
@repo_dependents [PhoenixKit.Supervisor, Oban]
@typedoc "A module extracted from a child spec, or `nil` when unrecognized."
@type child :: module() | nil
@doc """
Checks the child ordering in `source` relative to `repo_module`.
Returns:
* `{:ok, detail}` — the Repo precedes every Repo-dependent child that is
present (or none are present); `detail` is a human-readable summary.
* `{:misordered, [module]}` — the listed Repo-dependent modules appear
*before* the Repo. This is the crash case.
* `:no_repo_in_children` — `repo_module` wasn't found in the children list,
so ordering can't be judged (verify manually).
* `:no_children` — no `children` list could be located in the source.
"""
@spec check(String.t(), module()) ::
{:ok, String.t()} | {:misordered, [module()]} | :no_repo_in_children | :no_children
def check(source, repo_module) when is_binary(source) and is_atom(repo_module) do
case ordered_children(source) do
{:ok, mods} ->
case Enum.find_index(mods, &(&1 == repo_module)) do
nil ->
:no_repo_in_children
repo_index ->
offenders = Enum.filter(@repo_dependents, &before?(mods, &1, repo_index))
if offenders == [] do
{:ok, describe(mods, repo_module, repo_index)}
else
{:misordered, offenders}
end
end
:error ->
:no_children
end
end
@doc """
Extracts the ordered list of head modules from the host's `children` list.
Each element is the module heading a child spec (`MyApp.Repo`,
`{Oban, opts}``Oban`, `{Phoenix.PubSub, ...}``Phoenix.PubSub`), or
`nil` for a spec whose module can't be read cheaply (e.g. a `%{}` map spec).
Returns `:error` if no children list can be found.
"""
@spec ordered_children(String.t()) :: {:ok, [child()]} | :error
def ordered_children(source) when is_binary(source) do
with {:ok, ast} <- Code.string_to_quoted(source),
[list | _] <- children_lists(ast) do
{:ok, Enum.map(list, &child_head_module/1)}
else
_ -> :error
end
end
# ── Locating the children list ──────────────────────────────────────
# Collects every candidate children list in document order, from the two
# shapes the Phoenix generator and hand-written apps use:
# children = [ ... ]
# Supervisor.start_link([ ... ], opts)
defp children_lists(ast) do
{_ast, acc} = Macro.prewalk(ast, [], fn node, acc -> {node, collect_list(node, acc)} end)
acc |> Enum.reverse() |> Enum.map(fn {_pos, list} -> list end)
end
# `children = [ ... ]`
defp collect_list({:=, meta, [{:children, _, ctx}, list]}, acc)
when is_atom(ctx) and is_list(list),
do: [{meta[:line] || 0, list} | acc]
# `Supervisor.start_link([ ... ], opts)`
defp collect_list(
{{:., _, [{:__aliases__, _, [:Supervisor]}, :start_link]}, meta, [list | _]},
acc
)
when is_list(list),
do: [{meta[:line] || 0, list} | acc]
defp collect_list(_node, acc), do: acc
# ── Reading a child spec's head module ──────────────────────────────
# Bare module: `MyApp.Repo`
defp child_head_module({:__aliases__, _, parts}), do: safe_concat(parts)
# 3+-element tuple literal: `{Mod, a, b}` — module heads it.
defp child_head_module({:{}, _, [first | _]}), do: child_head_module(first)
# 2-element tuple literal: `{Oban, opts}` / `{Phoenix.PubSub, name: ...}`.
# (Alias/call/`{:{}}` nodes are all 3-tuples, so only genuine 2-tuples reach
# here.)
defp child_head_module({first, _second}), do: child_head_module(first)
defp child_head_module(_other), do: nil
defp safe_concat(parts) do
if Enum.all?(parts, &is_atom/1), do: Module.concat(parts), else: nil
end
# ── Helpers ─────────────────────────────────────────────────────────
defp before?(mods, module, repo_index) do
case Enum.find_index(mods, &(&1 == module)) do
nil -> false
index -> index < repo_index
end
end
defp describe(mods, repo_module, repo_index) do
present =
@repo_dependents
|> Enum.filter(&(&1 in mods))
|> Enum.map(&inspect/1)
dependents =
case present do
[] -> "no Repo-dependent children in the list"
names -> "before #{Enum.join(names, ", ")}"
end
"#{inspect(repo_module)} (position #{repo_index}) starts #{dependents}"
end
end