Can't call Phoenix.Endpoint from migration?

I’m trying to add a new field to a session table and generate a token to populate that field.

The problem I’m having is that this makes me call MyAppWeb.Endpoint in a migration file, which does not seem possible ? in order to sign the token I need the secret_key_base.

This will be used in controller actions in the future :

defmodule MyAppWeb.Helpers.Auth do
  def generate_auth_token(id) do
    Phoenix.Token.sign(MyAppWeb.Endpoint, "user auth", id)
  end
end

But to populate existing records, I’m trying to :

defmodule MyApp.Repo.Migrations.AddAuthTokenToSession do
  use Ecto.Migration
  import Ecto.Query, warn: false

  def change do
    alter table(:session) do
      add(:auth_token, :text)
    end

    add_auth_token_to_existing_sessions()
  end

  defp add_auth_token_to_existing_sessions do
    sessions = from(s in MyApp.Session |> MyApp.Repo.all()

    Enum.reduce(sessions, Ecto.Multi.new(), fn session, multi ->
      Ecto.Multi.update(
        multi,
        {:session, session.id},
        MyAppWeb.Helpers.Auth.generate_auth_token(session.id)
      )
    end)
    |> MyApp.Repo.transaction()
  end
end

I’m getting the following error :

** (ArgumentError) errors were found at the given arguments:

  * 1st argument: the table identifier does not refer to an existing ETS table

    (stdlib 3.17) :ets.lookup(MyAppWeb.Endpoint, :secret_key_base)
    (sonio_core 0.1.0) lib/phoenix/endpoint.ex:490: MyAppWeb.Endpoint.config/2
    (phoenix 1.5.12) lib/phoenix/token.ex:231: Phoenix.Token.get_endpoint_key_base/1
    (phoenix 1.5.12) lib/phoenix/token.ex:112: Phoenix.Token.sign/4
    priv/repo/migrations/20220215095601_add_auth_token_to_session.exs:20: anonymous fn/2 in MyApp.Repo.Migrations.AddAuthTokenToSession.add_auth_token_to_existing_sessions/0
    (elixir 1.12.3) lib/enum.ex:2385: Enum."-reduce/3-lists^foldl/2-0-"/3
    priv/repo/migrations/20220215095601_add_auth_token_to_session.exs:16: MyApp.Repo.Migrations.AddAuthTokenToSession.add_auth_token_to_existing_sessions/0
    (ecto_sql 3.7.0) lib/ecto/migration/runner.ex:279: Ecto.Migration.Runner.perform_operation/3
    (stdlib 3.17) timer.erl:166: :timer.tc/1
    (ecto_sql 3.7.0) lib/ecto/migration/runner.ex:25: Ecto.Migration.Runner.run/8
    (ecto_sql 3.7.0) lib/ecto/migrator.ex:324: Ecto.Migrator.attempt/8
    (ecto_sql 3.7.0) lib/ecto/migrator.ex:251: anonymous fn/5 in Ecto.Migrator.do_up/5
    (ecto_sql 3.7.0) lib/ecto/migrator.ex:295: anonymous fn/6 in Ecto.Migrator.async_migrate_maybe_in_transaction/7
    (ecto_sql 3.7.0) lib/ecto/adapters/sql.ex:1013: anonymous fn/3 in Ecto.Adapters.SQL.checkout_or_transaction/4
    (db_connection 2.4.1) lib/db_connection.ex:1531: DBConnection.run_transaction/4
    (ecto_sql 3.7.0) lib/ecto/migrator.ex:313: Ecto.Migrator.run_maybe_in_transaction/4
    (elixir 1.12.3) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (elixir 1.12.3) lib/task/supervised.ex:35: Task.Supervised.reply/5
    (stdlib 3.17) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
1 Like

Application is not started during an ecto migration - you can use Application.ensure_started/1 or Application.ensure_all_started/2.

You want to generate some auth token and save in database? Can’t you check this at runtime and generate instead of generating auth token using migration ? Like populate auth_token column with a seed value like gen_auth_once for eligible users. At runtime check for this same string - generate on demand and overwrite this seed value.

It’s better that migrations do not refer to external business logic, libraries, etc - as change in libraries or business logic may cause them to fail in future, execute multiple times, etc

2 Likes

Thank you very much ! didn’t know about Application.ensure_all_started, that’s great.

It’s better that migrations do not refer to external business logic, libraries, etc - as change in libraries or business logic may cause them to fail in future, execute multiple times, etc

Indeed, for that reason I believe I will simply run that piece of code in a console.

1 Like

Just curious - Phoenix token will expire after some time (in this case 1 day) - this is as good as user not having token? You are generating new token automatically if user has expired token ?

You can ignore this question if you want.

You are generating new token automatically if user has expired token ?

I simplified the code to ask for help but in the real implementation, a token lasts for 30 days and there is indeed a refresh strategy (basically refreshing the token when the user visits the app within 30 days)

1 Like