Packages

phoenix_kit

1.7.197
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

221 Versions

Jump to

Compare versions

9 files changed
+625 additions
-36 deletions
unknownCHANGELOG.md
File is too large to be displayed (100 KB limit).
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/BeamLabEU/phoenix_kit">>}]}.
2 2 {<<"name">>,<<"phoenix_kit">>}.
3 - {<<"version">>,<<"1.7.196">>}.
3 + {<<"version">>,<<"1.7.197">>}.
4 4 {<<"description">>,
5 5 <<65,32,102,111,117,110,100,97,116,105,111,110,32,102,111,114,32,98,117,105,
6 6 108,100,105,110,103,32,69,108,105,120,105,114,32,80,104,111,101,110,105,
  @@ -438,7 +438,7 @@
438 438 <<"lib/phoenix_kit/scheduled_jobs.ex">>,
439 439 <<"lib/phoenix_kit/application.ex">>,<<"lib/phoenix_kit/workers">>,
440 440 <<"lib/phoenix_kit/workers/oauth_config_loader.ex">>,
441 - <<"lib/phoenix_kit/install">>,
441 + <<"lib/phoenix_kit/install">>,<<"lib/phoenix_kit/install/child_order.ex">>,
442 442 <<"lib/phoenix_kit/install/igniter_compat.ex">>,
443 443 <<"lib/phoenix_kit/install/repo_detection.ex">>,
444 444 <<"lib/phoenix_kit/install/oauth_config.ex">>,
  @@ -586,6 +586,7 @@
586 586 <<"lib/phoenix_kit/migrations/postgres/v119.ex">>,
587 587 <<"lib/phoenix_kit/migrations/postgres/v06.ex">>,
588 588 <<"lib/phoenix_kit/migrations/postgres/v59.ex">>,
589 + <<"lib/phoenix_kit/migrations/postgres/v151.ex">>,
589 590 <<"lib/phoenix_kit/migrations/postgres/v11.ex">>,
590 591 <<"lib/phoenix_kit/migrations/postgres/v61.ex">>,
591 592 <<"lib/phoenix_kit/migrations/postgres/v96.ex">>,
  @@ -14,7 +14,10 @@ defmodule Mix.Tasks.PhoenixKit.Doctor do
14 14
15 15 ## Options
16 16
17 - * `--prefix` - Database schema prefix (default: "public")
17 + * `--prefix` - Database schema prefix. When omitted, resolves from
18 + `config :phoenix_kit, :prefix`, then `"public"` — the same resolution
19 + `mix phoenix_kit.update` / `--status` use, so a prefixed install is
20 + diagnosed against the schema it actually lives in.
18 21
19 22 ## Checks Performed
20 23
  @@ -23,20 +26,24 @@ defmodule Mix.Tasks.PhoenixKit.Doctor do
23 26 3. **Pool Configuration** — Pool size, checkout timeout, queue settings
24 27 4. **PgBouncer Detection** — Is PgBouncer between app and PostgreSQL?
25 28 5. **Migration State** — PhoenixKit version (COMMENT), schema_migrations alignment
26 - 6. **Pending Migrations** — Migration files not yet recorded in schema_migrations
27 - 7. **UUID Column Types** — Detects varchar uuid columns that crash Ecto on startup
28 - 8. **NULL UUIDs in FK Sources** — Detects NULL uuids that cause infinite backfill loops
29 - 9. **Orphaned FK References** — Detects orphaned rows that block FK constraint creation
30 - 10. **Lock Conflicts** — Any blocked or long-running queries?
31 - 11. **Orphaned Connections** — Idle-in-transaction or stuck connections
32 - 12. **Oban Configuration** — Queues and plugins that consume pool connections
33 - 13. **Supervisor Children** — What's running (update_mode vs full)?
34 - 14. **Update Mode** — Is update_mode active?
35 - 15. **daisyUI Version** — Is the host's vendored daisyUI recent enough?
29 + 6. **Schema Drift** — Columns a migration should have added but the DB lacks
30 + 7. **Pending Migrations** — Migration files not yet recorded in schema_migrations
31 + 8. **UUID Column Types** — Detects varchar uuid columns that crash Ecto on startup
32 + 9. **NULL UUIDs in FK Sources** — Detects NULL uuids that cause infinite backfill loops
33 + 10. **Orphaned FK References** — Detects orphaned rows that block FK constraint creation
34 + 11. **Lock Conflicts** — Any blocked or long-running queries?
35 + 12. **Orphaned Connections** — Idle-in-transaction or stuck connections
36 + 13. **Oban Configuration** — Queues and plugins that consume pool connections
37 + 14. **Supervisor Children** — What's running (update_mode vs full)?
38 + 15. **Child Start Order** — Does the Repo start before PhoenixKit/Oban in application.ex?
39 + 16. **Update Mode** — Is update_mode active?
40 + 17. **daisyUI Version** — Is the host's vendored daisyUI recent enough?
36 41 """
37 42
38 43 use Mix.Task
39 44
45 + alias PhoenixKit.Install.ChildOrder
46 + alias PhoenixKit.Install.PrefixConfig
40 47 alias PhoenixKit.Migrations.Postgres
41 48
42 49 @shortdoc "Diagnoses PhoenixKit installation, migration, and runtime issues"
  @@ -48,10 +55,21 @@ defmodule Mix.Tasks.PhoenixKit.Doctor do
48 55 def run(argv) do
49 56 {opts, _argv, _errors} = OptionParser.parse(argv, switches: @switches, aliases: @aliases)
50 57
51 - prefix = opts[:prefix] || "public"
52 -
53 58 # Start app with minimal footprint (same approach as phoenix_kit.update)
54 59 Mix.Task.run("app.config")
60 +
61 + # Resolve the prefix AFTER app.config loads config, so a configured
62 + # non-public prefix is honored — same resolution the updater/status use
63 + # (--prefix flag → config :phoenix_kit, :prefix → "public"). Reading
64 + # opts[:prefix] || "public" here queries the version marker at public and
65 + # reports a prefixed install as "not installed".
66 + prefix = PrefixConfig.resolve_prefix(opts)
67 +
68 + # Snapshot the host's Oban config BEFORE cap_repo_pool_size/1 zeroes its
69 + # queues/plugins (it does that to conserve connections in update_mode) —
70 + # otherwise the Oban Configuration check reports "0 queues, 0 plugins".
71 + oban_config = Application.get_env(Mix.Project.config()[:app], Oban)
72 +
55 73 cap_repo_pool_size(2)
56 74 Application.put_env(:phoenix_kit, :update_mode, true)
57 75 Mix.Task.run("app.start")
  @@ -64,14 +82,16 @@ defmodule Mix.Tasks.PhoenixKit.Doctor do
64 82 run_check("Pool Configuration", fn -> check_pool_config() end),
65 83 run_check("PgBouncer Detection", fn -> check_pgbouncer() end),
66 84 run_check("Migration State", fn -> check_migration_state(prefix) end),
85 + run_check("Schema Drift", fn -> check_schema_drift(prefix) end),
67 86 run_check("Pending Migrations", fn -> check_pending_migrations() end),
68 87 run_check("UUID Column Types", fn -> check_uuid_column_types(prefix) end),
69 88 run_check("NULL UUIDs in FK Sources", fn -> check_null_uuids(prefix) end),
70 89 run_check("Orphaned FK References", fn -> check_orphaned_fk_refs(prefix) end),
71 90 run_check("Lock Conflicts", fn -> check_lock_conflicts() end),
72 91 run_check("Orphaned Connections", fn -> check_orphaned_connections() end),
73 - run_check("Oban Configuration", fn -> check_oban_config() end),
92 + run_check("Oban Configuration", fn -> check_oban_config(oban_config) end),
74 93 run_check("PhoenixKit Supervisor", fn -> check_supervisor_state() end),
94 + run_check("Child Start Order", fn -> check_child_order() end),
75 95 run_check("Update Mode", fn -> check_update_mode() end),
76 96 run_check("daisyUI Version", fn -> check_daisyui() end)
77 97 ]
  @@ -226,6 +246,71 @@ defmodule Mix.Tasks.PhoenixKit.Doctor do
226 246 end
227 247 end
228 248
249 + # Columns a given migration version adds. If the version marker claims that
250 + # version (or higher) but the column is missing at the prefix, the install
251 + # drifted — the marker is ahead of the actual schema (e.g. a version renumber
252 + # that crossed an upgrade, or an earlier prefix-confused migration run). A
253 + # query that selects the column then crashes at runtime, and re-running the
254 + # migrator is a no-op because the marker already covers that version.
255 + @expected_columns [
256 + {150, "phoenix_kit_users_tokens", "browser"},
257 + {150, "phoenix_kit_users_tokens", "os"}
258 + ]
259 +
260 + defp check_schema_drift(prefix) do
261 + repo = get_repo!()
262 + escaped_prefix = String.replace(prefix, "'", "\\'")
263 + marker = get_comment_version(repo, escaped_prefix)
264 +
265 + if marker == 0 do
266 + {:pass, "PhoenixKit not installed at prefix #{inspect(prefix)} — nothing to check."}
267 + else
268 + missing =
269 + @expected_columns
270 + |> Enum.filter(fn {min_version, _t, _c} -> marker >= min_version end)
271 + |> Enum.reject(fn {_v, table, column} ->
272 + column_exists?(repo, escaped_prefix, table, column)
273 + end)
274 +
275 + report_schema_drift(missing, marker, prefix)
276 + end
277 + end
278 +
279 + defp report_schema_drift([], marker, _prefix),
280 + do: {:pass, "Columns expected at V#{marker} are present."}
281 +
282 + defp report_schema_drift(missing, marker, prefix) do
283 + names = Enum.map_join(missing, ", ", fn {v, t, c} -> "#{t}.#{c} (V#{v})" end)
284 + lowest = missing |> Enum.map(fn {v, _t, _c} -> v end) |> Enum.min()
285 + p = if prefix == "public", do: "public.", else: "#{prefix}."
286 +
287 + {:fail,
288 + "Marker says V#{marker} but these columns are missing: #{names}. The install drifted " <>
289 + "(marker ahead of schema). Roll the marker back one version and re-run the migrator — " <>
290 + "the column adds are idempotent (add_if_not_exists), so this is safe:\n" <>
291 + " COMMENT ON TABLE #{p}phoenix_kit IS '#{lowest - 1}';\n" <>
292 + " mix phoenix_kit.update#{prefix_flag(prefix)}"}
293 + end
294 +
295 + defp column_exists?(repo, escaped_prefix, table, column) do
296 + query = """
297 + SELECT EXISTS (
298 + SELECT FROM information_schema.columns
299 + WHERE table_schema = '#{escaped_prefix}'
300 + AND table_name = '#{table}'
301 + AND column_name = '#{column}'
302 + )
303 + """
304 +
305 + case repo.query(query, [], log: false) do
306 + {:ok, %{rows: [[true]]}} -> true
307 + _ -> false
308 + end
309 + end
310 +
311 + defp prefix_flag("public"), do: ""
312 + defp prefix_flag(prefix), do: " --prefix=#{prefix}"
313 +
229 314 defp check_pending_migrations do
230 315 repo = get_repo!()
231 316 migrations_path = Path.join(["priv", "repo", "migrations"])
  @@ -577,22 +662,20 @@ defmodule Mix.Tasks.PhoenixKit.Doctor do
577 662 end
578 663 end
579 664
580 - defp check_oban_config do
581 - app = Mix.Project.config()[:app]
665 + # Reports the Oban config snapshotted in run/1 BEFORE cap_repo_pool_size/1
666 + # zeroed its queues/plugins — reading it live here would always show 0/0.
667 + defp check_oban_config(nil), do: {:pass, "Oban not configured"}
582 668
583 - case Application.get_env(app, Oban) do
584 - nil ->
585 - {:pass, "Oban not configured"}
669 + defp check_oban_config(config) when is_list(config) do
670 + queues = Keyword.get(config, :queues, [])
671 + plugins = Keyword.get(config, :plugins, [])
586 672
587 - config ->
588 - queues = Keyword.get(config, :queues, [])
589 - plugins = Keyword.get(config, :plugins, [])
590 -
591 - {:pass,
592 - "#{length(queues)} queues, #{length(plugins)} plugins. Each active queue uses 1 pool connection."}
593 - end
673 + {:pass,
674 + "#{length(queues)} queues, #{length(plugins)} plugins. Each active queue uses 1 pool connection."}
594 675 end
595 676
677 + defp check_oban_config(_other), do: {:pass, "Oban configured (non-keyword config)"}
678 +
596 679 defp check_supervisor_state do
597 680 case Process.whereis(PhoenixKit.Supervisor) do
598 681 nil ->
  @@ -605,6 +688,45 @@ defmodule Mix.Tasks.PhoenixKit.Doctor do
605 688 end
606 689 end
607 690
691 + # Reads the host application.ex and verifies the Repo starts BEFORE
692 + # PhoenixKit.Supervisor and Oban — a child listed before the Repo crashes the
693 + # app at boot (PhoenixKit reads Settings from the DB; Oban needs the pool).
694 + # The runtime supervisor check above can't catch this (by the time doctor
695 + # runs, everything has already started), so we read the source order.
696 + defp check_child_order do
697 + repo = get_repo!()
698 +
699 + case host_application_source() do
700 + {:ok, path, source} ->
701 + where = Path.relative_to_cwd(path)
702 +
703 + case ChildOrder.check(source, repo) do
704 + {:ok, detail} ->
705 + {:pass, "#{detail} (#{where})"}
706 +
707 + {:misordered, mods} ->
708 + names = Enum.map_join(mods, ", ", &inspect/1)
709 +
710 + {:fail,
711 + "#{names} start BEFORE #{inspect(repo)} in #{where}. PhoenixKit.Supervisor " <>
712 + "reads Settings from the database and Oban needs the connection pool, so both " <>
713 + "must be listed AFTER your Repo. Move #{inspect(repo)} above them in the " <>
714 + "children list to fix the boot crash."}
715 +
716 + :no_repo_in_children ->
717 + {:warn,
718 + "Couldn't find #{inspect(repo)} in the children list of #{where} — verify " <>
719 + "PhoenixKit.Supervisor and Oban are started after your Repo."}
720 +
721 + :no_children ->
722 + {:warn, "Couldn't locate a children list in #{where} to verify start order."}
723 + end
724 +
725 + :error ->
726 + {:warn, "Couldn't locate your application.ex to verify child start order."}
727 + end
728 + end
729 +
608 730 defp check_update_mode do
609 731 update_mode = Application.get_env(:phoenix_kit, :update_mode, false)
610 732
  @@ -626,6 +748,40 @@ defmodule Mix.Tasks.PhoenixKit.Doctor do
626 748 end
627 749 end
628 750
751 + # Locate the host's application.ex — first via the compiled application
752 + # module's source path, then the conventional lib/<app>/application.ex.
753 + defp host_application_source do
754 + app = Mix.Project.config()[:app]
755 +
756 + candidates =
757 + [
758 + case Application.spec(app, :mod) do
759 + {mod, _args} -> module_source(mod)
760 + _ -> nil
761 + end,
762 + Path.join(["lib", "#{app}", "application.ex"])
763 + ]
764 + |> Enum.reject(&is_nil/1)
765 +
766 + Enum.find_value(candidates, :error, fn path ->
767 + case File.read(path) do
768 + {:ok, source} -> {:ok, path, source}
769 + _ -> nil
770 + end
771 + end)
772 + end
773 +
774 + defp module_source(mod) do
775 + with {:module, _} <- Code.ensure_loaded(mod),
776 + source when not is_nil(source) <- mod.module_info(:compile)[:source] do
777 + to_string(source)
778 + else
779 + _ -> nil
780 + end
781 + rescue
782 + _ -> nil
783 + end
784 +
629 785 defp cap_repo_pool_size(pool_size) do
630 786 app = Mix.Project.config()[:app]
631 787 repos = Application.get_env(app, :ecto_repos, [])
  @@ -0,0 +1,153 @@
1 + defmodule PhoenixKit.Install.ChildOrder do
2 + @moduledoc """
3 + Reads a host application's `application.ex` and checks that the Ecto Repo
4 + starts BEFORE `PhoenixKit.Supervisor` and `Oban` in the supervision
5 + `children` list.
6 +
7 + `PhoenixKit.Supervisor` reads Settings / OAuth configuration from the
8 + database as it boots, and Oban opens a connection pool against the same
9 + Repo — so both MUST appear after the Repo in the children list. A
10 + mis-ordered list crashes the app at startup (typically an Oban crash-loop:
11 + the pool has no database to reach yet).
12 +
13 + `mix phoenix_kit.install` positions its child after the detected Repo, but a
14 + hand-edited `application.ex` — or an Igniter anchor miss that prepends the
15 + child instead of inserting it after the Repo — can regress the order. This
16 + module is the deterministic safety net: `mix phoenix_kit.doctor` runs
17 + `check/2` against the host's source so both fresh and existing installs are
18 + caught, independent of how the children ended up ordered.
19 +
20 + Everything here is a pure function over source text — no application boot,
21 + no database — so it is unit-testable in isolation.
22 + """
23 +
24 + # Children that depend on the Repo already being started.
25 + @repo_dependents [PhoenixKit.Supervisor, Oban]
26 +
27 + @typedoc "A module extracted from a child spec, or `nil` when unrecognized."
28 + @type child :: module() | nil
29 +
30 + @doc """
31 + Checks the child ordering in `source` relative to `repo_module`.
32 +
33 + Returns:
34 +
35 + * `{:ok, detail}` — the Repo precedes every Repo-dependent child that is
36 + present (or none are present); `detail` is a human-readable summary.
37 + * `{:misordered, [module]}` — the listed Repo-dependent modules appear
38 + *before* the Repo. This is the crash case.
39 + * `:no_repo_in_children` — `repo_module` wasn't found in the children list,
40 + so ordering can't be judged (verify manually).
41 + * `:no_children` — no `children` list could be located in the source.
42 + """
43 + @spec check(String.t(), module()) ::
44 + {:ok, String.t()} | {:misordered, [module()]} | :no_repo_in_children | :no_children
45 + def check(source, repo_module) when is_binary(source) and is_atom(repo_module) do
46 + case ordered_children(source) do
47 + {:ok, mods} ->
48 + case Enum.find_index(mods, &(&1 == repo_module)) do
49 + nil ->
50 + :no_repo_in_children
51 +
52 + repo_index ->
53 + offenders = Enum.filter(@repo_dependents, &before?(mods, &1, repo_index))
54 +
55 + if offenders == [] do
56 + {:ok, describe(mods, repo_module, repo_index)}
57 + else
58 + {:misordered, offenders}
59 + end
60 + end
61 +
62 + :error ->
63 + :no_children
64 + end
65 + end
66 +
67 + @doc """
68 + Extracts the ordered list of head modules from the host's `children` list.
69 +
70 + Each element is the module heading a child spec (`MyApp.Repo`,
71 + `{Oban, opts}``Oban`, `{Phoenix.PubSub, ...}``Phoenix.PubSub`), or
72 + `nil` for a spec whose module can't be read cheaply (e.g. a `%{}` map spec).
73 + Returns `:error` if no children list can be found.
74 + """
75 + @spec ordered_children(String.t()) :: {:ok, [child()]} | :error
76 + def ordered_children(source) when is_binary(source) do
77 + with {:ok, ast} <- Code.string_to_quoted(source),
78 + [list | _] <- children_lists(ast) do
79 + {:ok, Enum.map(list, &child_head_module/1)}
80 + else
81 + _ -> :error
82 + end
83 + end
84 +
85 + # ── Locating the children list ──────────────────────────────────────
86 +
87 + # Collects every candidate children list in document order, from the two
88 + # shapes the Phoenix generator and hand-written apps use:
89 + # children = [ ... ]
90 + # Supervisor.start_link([ ... ], opts)
91 + defp children_lists(ast) do
92 + {_ast, acc} = Macro.prewalk(ast, [], fn node, acc -> {node, collect_list(node, acc)} end)
93 + acc |> Enum.reverse() |> Enum.map(fn {_pos, list} -> list end)
94 + end
95 +
96 + # `children = [ ... ]`
97 + defp collect_list({:=, meta, [{:children, _, ctx}, list]}, acc)
98 + when is_atom(ctx) and is_list(list),
99 + do: [{meta[:line] || 0, list} | acc]
100 +
101 + # `Supervisor.start_link([ ... ], opts)`
102 + defp collect_list(
103 + {{:., _, [{:__aliases__, _, [:Supervisor]}, :start_link]}, meta, [list | _]},
104 + acc
105 + )
106 + when is_list(list),
107 + do: [{meta[:line] || 0, list} | acc]
108 +
109 + defp collect_list(_node, acc), do: acc
110 +
111 + # ── Reading a child spec's head module ──────────────────────────────
112 +
113 + # Bare module: `MyApp.Repo`
114 + defp child_head_module({:__aliases__, _, parts}), do: safe_concat(parts)
115 +
116 + # 3+-element tuple literal: `{Mod, a, b}` — module heads it.
117 + defp child_head_module({:{}, _, [first | _]}), do: child_head_module(first)
118 +
119 + # 2-element tuple literal: `{Oban, opts}` / `{Phoenix.PubSub, name: ...}`.
120 + # (Alias/call/`{:{}}` nodes are all 3-tuples, so only genuine 2-tuples reach
121 + # here.)
122 + defp child_head_module({first, _second}), do: child_head_module(first)
123 +
124 + defp child_head_module(_other), do: nil
125 +
126 + defp safe_concat(parts) do
127 + if Enum.all?(parts, &is_atom/1), do: Module.concat(parts), else: nil
128 + end
129 +
130 + # ── Helpers ─────────────────────────────────────────────────────────
131 +
132 + defp before?(mods, module, repo_index) do
133 + case Enum.find_index(mods, &(&1 == module)) do
134 + nil -> false
135 + index -> index < repo_index
136 + end
137 + end
138 +
139 + defp describe(mods, repo_module, repo_index) do
140 + present =
141 + @repo_dependents
142 + |> Enum.filter(&(&1 in mods))
143 + |> Enum.map(&inspect/1)
144 +
145 + dependents =
146 + case present do
147 + [] -> "no Repo-dependent children in the list"
148 + names -> "before #{Enum.join(names, ", ")}"
149 + end
150 +
151 + "#{inspect(repo_module)} (position #{repo_index}) starts #{dependents}"
152 + end
153 + end
  @@ -1309,7 +1309,7 @@ defmodule PhoenixKit.Migrations.Postgres do
1309 1309 alias PhoenixKit.Migrations.Postgres.Helpers
1310 1310
1311 1311 @initial_version 1
1312 - @current_version 150
1312 + @current_version 151
1313 1313 @default_prefix "public"
1314 1314
1315 1315 # First version whose SQL references uuid_generate_v7(). Chains that
Loading more files…