# Adapted from https://github.com/electric-sql/phoenix_sync # Licensed under the Apache License 2.0 # # Modified to address an issue where `mix igniter.install phoenix_sync` # fails due to `Phoenix.Sync.MixProject` not being available. defmodule Mix.Tasks.PhoenixSyncFix.Install.Docs do @moduledoc false @spec short_doc() :: String.t() def short_doc do "Install Phoenix.Sync into an existing Phoenix or Plug application" end @spec example() :: String.t() def example do "mix phoenix_sync_fix.install --sync-mode embedded" end @spec long_doc() :: String.t() def long_doc do """ #{short_doc()} Usually invoked using `igniter.install`: ```sh mix igniter.install phoenix_sync_fix --sync-mode embedded ``` But can be invoked directly if `:phoenix_sync_fix` is already a dependency: ```sh #{example()} ``` ## Options * `--sync-mode` - How to connect to Electric, either `embedded` or `http`. - `embedded` - `:electric` will be added as a dependency and will connect to the database your repo is configured for. - `http` - You'll need to specify the `--sync-url` to the remote Electric server. ### Options for `embedded` mode * `--no-sync-sandbox` - Disable the test sandbox ### Options for `http` mode * `--sync-url` (required) - The URL of the Electric server, required for `http` mode. """ end end if Code.ensure_loaded?(Igniter) do defmodule Mix.Tasks.PhoenixSyncFix.Install do @shortdoc "#{__MODULE__.Docs.short_doc()}" @moduledoc __MODULE__.Docs.long_doc() use Igniter.Mix.Task require Igniter.Code.Function @impl Igniter.Mix.Task def info(_argv, _composing_task) do %Igniter.Mix.Task.Info{ group: :phoenix_sync_fix, adds_deps: [], installs: [], example: __MODULE__.Docs.example(), only: nil, positional: [], composes: [], schema: [ sync_mode: :string, sync_url: :string, sync_sandbox: :boolean ], defaults: [], aliases: [], required: [:sync_mode] } end @valid_modes ~w[embedded http] @impl Igniter.Mix.Task def igniter(igniter) do project = PhoenixSyncFix.MixProject.project() # Puedo usar MixProject.project() sin problemas IO.puts(String.duplicate("*", 130)) IO.inspect(project, label: "project") IO.puts(String.duplicate("*", 130)) {:ok, mode} = Keyword.fetch(igniter.args.options, :sync_mode) if mode not in @valid_modes do Igniter.add_issue( igniter, "mode #{inspect(mode)} is invalid, valid modes are: #{@valid_modes |> Enum.join(", ")}" ) else igniter |> ensure_phoenix_sync_dep() |> add_dependencies(mode) end end defp ensure_phoenix_sync_dep(igniter) do if Igniter.Project.Deps.has_dep?(igniter, :phoenix_sync) do igniter else Igniter.Project.Deps.add_dep( igniter, {:phoenix_sync, "~> 0.6"}, error?: true ) end end defp add_dependencies(igniter, "http") do case Keyword.fetch(igniter.args.options, :sync_url) do {:ok, url} -> igniter |> base_configuration(:http) |> Igniter.Project.Config.configure_new( "config.exs", :phoenix_sync, [:url], url ) |> Igniter.Project.Config.configure_new( "config.exs", :phoenix_sync, [:credentials], secret: "MY_SECRET", source_id: "00000000-0000-0000-0000-000000000000" ) |> configure_endpoint() :error -> Igniter.add_issue(igniter, "`--sync-url` is required for :http mode") end end defp add_dependencies(igniter, "embedded") do igniter |> Igniter.Project.Deps.add_dep({:electric, required_electric_version()}, error?: true) |> then(fn igniter -> if igniter.assigns[:test_mode?] do igniter else Igniter.apply_and_fetch_dependencies(igniter) end end) |> base_configuration(:embedded) |> find_repo() |> configure_repo() |> configure_endpoint() end defp configure_endpoint(igniter) do application = Igniter.Project.Application.app_module(igniter) case Igniter.Libs.Phoenix.select_endpoint(igniter) do {igniter, nil} -> configure_plug_app(igniter, application) {igniter, endpoint} -> configure_phoenix_endpoint(igniter, application, endpoint) end end defp configure_plug_app(igniter, application) do # find plug module in application children and add config there set_plug_opts(igniter, application, [Plug.Cowboy, Bandit], fn zipper -> with {:ok, zipper} <- Igniter.Code.Tuple.tuple_elem(zipper, 1) do Igniter.Code.Keyword.set_keyword_key( zipper, :plug, nil, fn zipper -> if Igniter.Code.Tuple.tuple?(zipper) do with {:ok, zipper} <- Igniter.Code.Tuple.tuple_elem(zipper, 1) do Igniter.Code.Keyword.set_keyword_key( zipper, :phoenix_sync, quote(do: Phoenix.Sync.plug_opts()) ) end else with {:ok, plug} <- Igniter.Code.Common.expand_literal(zipper) do {:ok, zipper |> Sourceror.Zipper.search_pattern("#{inspect(plug)}") |> Igniter.Code.Common.replace_code( "{#{inspect(plug)}, phoenix_sync: Phoenix.Sync.plug_opts()}" )} end end end ) end end) end defp configure_phoenix_endpoint(igniter, application, endpoint) do set_plug_opts(igniter, application, [endpoint], fn zipper -> # gets called with a zipper on the endpoint module in the list of children if Igniter.Code.Tuple.tuple?(zipper) do with {:ok, zipper} <- Igniter.Code.Tuple.tuple_elem(zipper, 1) do Igniter.Code.Keyword.set_keyword_key( zipper, :phoenix_sync, quote(do: Phoenix.Sync.plug_opts()) ) end else # the search pattern call results in replacing the module name with # the configuration whilst preserving the preceding comments {:ok, zipper |> Sourceror.Zipper.search_pattern("#{inspect(endpoint)}") |> Igniter.Code.Common.replace_code( "{#{inspect(endpoint)}, phoenix_sync: Phoenix.Sync.plug_opts()}" )} end end) end defp configure_repo(%{issues: [_ | _] = _issues} = igniter) do igniter end defp configure_repo(igniter) do case igniter.assigns do %{repo: repo} when is_atom(repo) -> igniter = igniter |> Igniter.Project.Config.configure_new("config.exs", :phoenix_sync, [:repo], repo) |> Igniter.Project.Config.configure_new("test.exs", :phoenix_sync, [:mode], :sandbox) |> Igniter.Project.Config.configure_new( "test.exs", :phoenix_sync, [:env], {:code, quote(do: config_env())} ) enable_sandbox? = Keyword.get(igniter.args.options, :sync_sandbox, true) if enable_sandbox? do igniter |> Igniter.Project.Module.find_and_update_module!( repo, fn zipper -> with :error <- Igniter.Code.Module.move_to_use(zipper, Phoenix.Sync.Sandbox.Postgres), {:ok, zipper} <- Igniter.Code.Function.move_to_function_call(zipper, :use, [2]), {:ok, zipper} <- Igniter.Code.Function.update_nth_argument(zipper, 1, fn zipper -> adapter = quote(do: Phoenix.Sync.Sandbox.Postgres.adapter()) Igniter.Code.Keyword.set_keyword_key( zipper, :adapter, adapter, fn z -> {:ok, Igniter.Code.Common.replace_code(z, adapter)} end ) end), {:ok, zipper} <- Igniter.Code.Module.move_to_use(zipper, Ecto.Repo), zipper <- Igniter.Code.Common.add_code(zipper, "use Phoenix.Sync.Sandbox.Postgres", placement: :before ) do {:ok, zipper} end end ) else igniter end _ -> igniter |> Igniter.add_notice("No Ecto.Repo found, adding example `connection_opts` to config") |> Igniter.Project.Config.configure_new( "config.exs", :phoenix_sync, [:connection_opts], {:code, Sourceror.parse_string!(""" # add your real database connection details [ username: "your_username", password: "your_password", hostname: "localhost", database: "your_database", port: 5432, # sslmode can be: :disable, :allow, :prefer or :require sslmode: :prefer ] """)} ) end end defp base_configuration(igniter, mode) do igniter |> Igniter.Project.Config.configure_new( "config.exs", :phoenix_sync, [:mode], mode ) |> Igniter.Project.Config.configure_new( "config.exs", :phoenix_sync, [:env], {:code, quote(do: config_env())} ) end defp required_electric_version do with {:ok, _vsn} <- Application.ensure_loaded(:phoenix_sync), deps when is_list(deps) <- phoenix_sync_deps() do case Enum.find(deps, &match?({:electric, _, _}, &1)) do {:electric, requirement, _opts} -> requirement {:electric, requirement} -> requirement _ -> default_requirement() end else _ -> default_requirement() end end defp phoenix_sync_deps do case Application.spec(:phoenix_sync, :modules) do nil -> [] _ -> case :code.which(Phoenix.Sync.MixProject) do :non_existing -> [] _ -> Phoenix.Sync.MixProject.project() |> Keyword.get(:deps, []) end end end defp default_requirement do ">= 1.1.9 and <= 1.1.10" end defp find_repo(igniter) do case Igniter.Libs.Ecto.select_repo(igniter) do {igniter, nil} -> Igniter.add_notice( igniter, """ No Ecto.Repo found in application environment. To use `embedded` mode you must add `connection_opts` to your config, e.g. config :phoenix_sync, env: config_env(), mode: :embedded, connection_opts: [ username: "your_username", password: "your_password", hostname: "localhost", database: "your_database", port: 5432 ] """ ) {igniter, repo} -> Igniter.assign(igniter, :repo, repo) end end defp set_plug_opts(igniter, application, modify_modules, updater) when is_list(modify_modules) do Igniter.Project.Module.find_and_update_module!(igniter, application, fn zipper -> with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :start, 2), {:ok, zipper} <- Igniter.Code.Function.move_to_function_call_in_current_scope( zipper, :=, [2], fn call -> Igniter.Code.Function.argument_matches_pattern?( call, 0, {:children, _, context} when is_atom(context) ) && Igniter.Code.Function.argument_matches_pattern?(call, 1, v when is_list(v)) end ), {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1) do case Igniter.Code.List.move_to_list_item(zipper, fn item -> case extract_child_module(item) do {:ok, child_module} -> Enum.any?(modify_modules, fn modify_module -> Igniter.Code.Common.nodes_equal?(child_module, modify_module) end) :error -> false end end) do {:ok, zipper} -> updater.(zipper) :error -> {:warning, """ Could not find a suitable `children = [...]` assignment in the `start` function of the `#{inspect(application)}` module. Please add `phoenix_sync: Phoenix.Sync.plug_opts()` to your Phoenix endpoint or Plug module configuration """} end else _ -> {:warning, """ Could not find a `children = [...]` assignment in the `start` function of the `#{inspect(application)}` module. Please add `phoenix_sync: Phoenix.Sync.plug_opts()` to your Phoenix endpoint or Plug module configuration """} end end) end defp extract_child_module(zipper) do if Igniter.Code.Tuple.tuple?(zipper) do with {:ok, elem} <- Igniter.Code.Tuple.tuple_elem(zipper, 0) do {:ok, Igniter.Code.Common.expand_alias(elem)} end else {:ok, Igniter.Code.Common.expand_alias(zipper)} end end end else defmodule Mix.Tasks.PhoenixSyncFix.Install do @shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use" @moduledoc __MODULE__.Docs.long_doc() use Mix.Task @impl Mix.Task def run(_argv) do Mix.shell().error(""" The task 'phoenix_sync_fix.install' requires igniter. Please install igniter and try again. For more information, see: https://hexdocs.pm/igniter/readme.html#installation """) exit({:shutdown, 1}) end end end