+ <.link
+ id="users"
+ navigate={~p"/accounts/users"}
+ class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700 hover:underline"
+ >
+ <.icon name="hero-users" class="h-4 w-4" />
+ {gettext("Users")}
+
+ <.link
+ id="user-invitations"
+ navigate={~p"/accounts/users/invitations"}
+ class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700 hover:underline"
+ >
+ <.icon name="hero-message" class="h-4 w-4" />
+ {gettext("Invitations")}
+
<.link
id="categories"
navigate={~p"/categories"}
diff --git a/lib/helpcenter_web/controllers/auth_controller.ex b/lib/helpcenter_web/controllers/auth_controller.ex
index 50939e5..a7fcd77 100644
--- a/lib/helpcenter_web/controllers/auth_controller.ex
+++ b/lib/helpcenter_web/controllers/auth_controller.ex
@@ -48,7 +48,7 @@ defmodule HelpcenterWeb.AuthController do
return_to = get_session(conn, :return_to) || ~p"/"
conn
- |> clear_session()
+ |> clear_session(:helpcenter)
|> put_flash(:info, "You are now signed out")
|> redirect(to: return_to)
end
diff --git a/lib/helpcenter_web/controllers/team_invitation_acceptance_controller.ex b/lib/helpcenter_web/controllers/team_invitation_acceptance_controller.ex
new file mode 100644
index 0000000..6a64717
--- /dev/null
+++ b/lib/helpcenter_web/controllers/team_invitation_acceptance_controller.ex
@@ -0,0 +1,87 @@
+# lib/helpcenter_web/controllers/team_invitation_acceptance_controller.ex
+defmodule HelpcenterWeb.TeamInvitationAcceptanceController do
+ use HelpcenterWeb, :controller
+
+ @doc """
+ Accepts a team invitation using a tenant and token, updating the invitation status.
+ Redirects to the homepage with appropriate flash messages based on the outcome.
+ """
+ def accept(conn, %{"tenant" => tenant, "token" => token}) do
+ # 1. Check if the invitation exists and is valid
+ # 2. If valid, accept inviation(creates account behind the scenes)
+ # 3. Redirect to homepage with success message
+ with {:ok, invitation} <- fetch_invitation(tenant, token),
+ {:ok, _updated_invitation} <- accept_invitation(invitation, tenant) do
+ conn
+ |> put_flash(:info, gettext("Invitation accepted"))
+ |> redirect(to: ~p"/")
+ else
+ {:error, error} ->
+ conn
+ |> put_flash(:error, format_error_message(error))
+ |> redirect(to: ~p"/")
+ end
+ end
+
+ def reject(conn, %{"tenant" => tenant, "token" => token}) do
+ # 1. Check if the invitation exists and is valid
+ # 2. If valid, reject inviation
+ # 3. Redirect to homepage with success message
+
+ with {:ok, invitation} <- fetch_invitation(tenant, token),
+ {:ok, _updated_invitation} <- reject_invitation(invitation, tenant) do
+ conn
+ |> put_flash(:info, gettext("Invitation rejected"))
+ |> redirect(to: ~p"/")
+ else
+ {:error, error} ->
+ conn
+ |> put_flash(:error, format_error_message(error))
+ |> redirect(to: ~p"/")
+ end
+ end
+
+ defp fetch_invitation(tenant, token) do
+ require Ash.Query
+
+ options = [tenant: tenant, authorize?: false]
+
+ Helpcenter.Accounts.Invitation
+ |> Ash.Query.filter(token == ^token)
+ |> Ash.read_first(options)
+ |> case do
+ {:ok, nil} -> {:error, :invitation_not_found}
+ other -> other
+ end
+ end
+
+ defp accept_invitation(invitation, tenant) do
+ options = [tenant: tenant, authorize?: false]
+
+ invitation
+ |> Ash.Changeset.for_update(:accept, %{}, options)
+ |> Ash.update()
+ end
+
+ defp reject_invitation(invitation, tenant) do
+ options = [tenant: tenant, authorize?: false]
+
+ invitation
+ |> Ash.Changeset.for_update(:accept, %{}, options)
+ |> Ash.update()
+ end
+
+ defp format_error_message(:invitation_not_found) do
+ gettext("Invitation not found")
+ end
+
+ defp format_error_message(%Ash.Error.Invalid{errors: errors}) do
+ errors
+ |> Enum.map(& &1.message)
+ |> Enum.join(", ")
+ end
+
+ defp format_error_message(_other) do
+ gettext("Unable to accept invitation")
+ end
+end
diff --git a/lib/helpcenter_web/controllers/team_invitation_acceptance_controller_test.exs b/lib/helpcenter_web/controllers/team_invitation_acceptance_controller_test.exs
new file mode 100644
index 0000000..ea751e5
--- /dev/null
+++ b/lib/helpcenter_web/controllers/team_invitation_acceptance_controller_test.exs
@@ -0,0 +1,47 @@
+# lib/helpcenter_web/controllers/team_invitation_acceptance_controller_test.exs
+defmodule HelpcenterWeb.TeamInvitationAcceptanceControllerTEst do
+ use HelpcenterWeb.ConnCase
+
+ defp create_group!(actor) do
+ group_attrs = %{name: "Accountants", description: "Can manage billing in the system"}
+
+ Ash.create!(
+ Helpcenter.Accounts.Group,
+ group_attrs,
+ actor: actor,
+ authorize?: false
+ )
+ end
+
+ describe "Team invitation acceptance controller" do
+ test "User can accept an invitation", %{conn: conn} do
+ # 1. Create auser
+ # 2. Add user to the team
+ # 3. Get the invitation token and send it in the URL
+ # 4. Confirm the notifiaction flash
+ # 5. Ensure that the user has been added to the team
+ actor = create_user()
+ group = create_group!(actor)
+ invite_attributes = %{email: "john@example.com", group_id: group.id}
+
+ invitation =
+ Helpcenter.Accounts.Invitation
+ |> Ash.Changeset.for_create(:create, invite_attributes, actor: actor)
+ |> Ash.create!()
+
+ conn =
+ conn
+ |> get(~p"/accounts/users/invitations/#{invitation.team}/#{invitation.token}/accept")
+
+ assert redirected_to(conn) == ~p"/"
+
+ # Confirm the invitation has been accepted
+ require Ash.Query
+
+ assert Helpcenter.Accounts.Invitation
+ |> Ash.Query.filter(token == ^invitation.token)
+ |> Ash.Query.filter(status == :accepted)
+ |> Ash.exists?(actor: actor)
+ end
+ end
+end
diff --git a/lib/helpcenter_web/live/accounts/users/user_invitations_live.ex b/lib/helpcenter_web/live/accounts/users/user_invitations_live.ex
new file mode 100644
index 0000000..18b21d1
--- /dev/null
+++ b/lib/helpcenter_web/live/accounts/users/user_invitations_live.ex
@@ -0,0 +1,33 @@
+# lib/helpcenter_web/live/accounts/users/user_invitations_live.ex
+defmodule HelpcenterWeb.Accounts.Users.UserInvitationsLive do
+ use HelpcenterWeb, :live_view
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ <:col :let={row} label="Email" field="email" filter sort>
+ {Phoenix.Naming.humanize(row.email)}
+
+ <:col :let={row} label="Status" field="status" filter sort>
+ {Phoenix.Naming.humanize(row.status)}
+
+ <:col :let={row} label="Team">{Phoenix.Naming.humanize(row.team)}
+
+ """
+ end
+
+ @impl true
+ def mount(_params, _sessions, socket) do
+ socket
+ |> assign(:page_title, gettext("User Invitations"))
+ |> ok()
+ end
+end
diff --git a/lib/helpcenter_web/live/accounts/users/user_invitations_live/invite_new_user_form.ex b/lib/helpcenter_web/live/accounts/users/user_invitations_live/invite_new_user_form.ex
new file mode 100644
index 0000000..b208150
--- /dev/null
+++ b/lib/helpcenter_web/live/accounts/users/user_invitations_live/invite_new_user_form.ex
@@ -0,0 +1,124 @@
+# lib/helpcenter_web/live/accounts/users/user_invitations_live/invite_new_user_form.ex
+defmodule HelpcenterWeb.Accounts.Users.UserInvitationsLive.InviteNewUserForm do
+ @moduledoc """
+ LiveComponent that renders a form to invite a new user to the current user's team.
+ The form includes fields for the user's email and the access group they should be assigned to.
+ When the form is submitted, an invitation is created in the system, and an invitation email
+ is sent to the provided email address.
+
+ This component is designed to be used within a modal dialog, which can be triggered by a button
+ elsewhere in the application. The modal button is this compoent
+ """
+ use HelpcenterWeb, :live_component
+
+ @doc """
+ This a wrapper used to access this component like a static component
+ in the template.
+ """
+ attr :actor, Helpcenter.Accounts.User, required: true
+
+ def form(assigns) do
+ ~H"""
+ <.live_component id={Ash.UUIDv7.generate()} actor={@actor} module={__MODULE__} />
+ """
+ end
+
+ attr :id, :string, default: Ash.UUIDv7.generate()
+ attr :actor, Helpcenter.Accounts.User, required: true
+
+ def render(assigns) do
+ ~H"""
+
+ <.button phx-click={show_modal("invite-user-modal")} id="invite-user-modal-button">
+ {gettext("Invite a user")}
+
+ <.modal id="invite-user-modal">
+
+
+ {gettext("Invite a new member to your team")}
+
+
+ {gettext(
+ "Invite a new user to your team by entering their email address below. They will receive an invitation email with instructions on how to join your team."
+ )}
+
+
+
+ <.simple_form for={@form} phx-change="validate" phx-submit="save" phx-target={@myself}>
+ <.input field={@form[:email]} type="email" label="New user Email" />
+ <.input
+ field={@form[:group_id]}
+ type="select"
+ options={@access_groups}
+ label={gettext("Select Access Group")}
+ />
+
+ <.button>
+ {gettext("Invite User")} <.icon name="hero-paper-airplane-solid" class="ml-2 h-5 w-5" />
+
+
+
+
+
+ """
+ end
+
+ @impl true
+ def update(assigns, socket) do
+ socket
+ |> assign(assigns)
+ |> assign_access_groups()
+ |> assign_form()
+ |> ok()
+ end
+
+ @impl true
+ def handle_event("validate", %{"form" => params}, socket) do
+ form = AshPhoenix.Form.validate(socket.assigns.form, params)
+
+ socket
+ |> assign(:form, form)
+ |> noreply()
+ end
+
+ @impl true
+ def handle_event("save", %{"form" => params}, socket) do
+ case AshPhoenix.Form.submit(socket.assigns.form, params: params) do
+ {:ok, _invitation} ->
+ socket
+ |> put_flash(:info, "Invitation sent successfully")
+ |> push_event("js-exec", %{to: "#invite-user-modal", attr: "data-cancel"})
+ |> redirect(to: ~p"/accounts/users/invitations")
+ |> noreply()
+
+ {:error, form} ->
+ socket
+ |> assign(:form, form)
+ |> noreply()
+ end
+ end
+
+ defp assign_form(socket) do
+ assign(socket, :form, get_form(socket.assigns))
+ end
+
+ # Prevents the form from being re-created on every update
+ defp get_form(%{form: form}) when is_map(form), do: form
+
+ defp get_form(%{actor: actor}) do
+ Helpcenter.Accounts.Invitation
+ |> AshPhoenix.Form.for_create(:create, actor: actor)
+ |> to_form()
+ end
+
+ defp assign_access_groups(socket) do
+ %{actor: actor} = socket.assigns
+
+ groups =
+ Helpcenter.Accounts.Group
+ |> Ash.read!(tenant: actor.current_team, authorize?: false)
+ |> Enum.map(&{&1.name, &1.id})
+
+ assign(socket, :access_groups, groups)
+ end
+end
diff --git a/lib/helpcenter_web/live/accounts/users/user_invitations_live/invite_new_user_form_test.exs b/lib/helpcenter_web/live/accounts/users/user_invitations_live/invite_new_user_form_test.exs
new file mode 100644
index 0000000..e48070e
--- /dev/null
+++ b/lib/helpcenter_web/live/accounts/users/user_invitations_live/invite_new_user_form_test.exs
@@ -0,0 +1,57 @@
+defmodule HelpcenterWeb.Accounts.Users.UserInvitationsLive.InviteNewUserFormTest do
+ use HelpcenterWeb.ConnCase
+ alias HelpcenterWeb.Accounts.Users.UserInvitationsLive.InviteNewUserForm
+
+ defp create_group!(actor) do
+ group_attrs = %{name: "Accountants", description: "Can manage billing in the system"}
+
+ Ash.create!(
+ Helpcenter.Accounts.Group,
+ group_attrs,
+ actor: actor,
+ authorize?: false
+ )
+ end
+
+ describe "Invite New User Form" do
+ test "Invite user form renders successfully" do
+ assigns = %{id: Ash.UUIDv7.generate(), actor: create_user()}
+ html = render_component(InviteNewUserForm, assigns)
+
+ assert html =~ "email"
+ assert html =~ "group_id"
+ assert html =~ "Invite User"
+ assert html =~ "Invite a new member to your team"
+ end
+
+ test "New user can be invited successfully", %{conn: conn} do
+ user = create_user()
+ group = create_group!(user)
+
+ {:ok, view, html} =
+ conn
+ |> login(user)
+ |> live("/accounts/users/invitations")
+
+ # Confirm invite user button is present
+ assert html =~ "Invite a user"
+ assert has_element?(view, "#invite-user-modal-button")
+
+ # Confirm submitting the form works
+ params = %{email: "tester@example.com", group_id: group.id}
+
+ view
+ |> form("#invite-user-modal form", form: params)
+ |> render_submit()
+
+ # Confirm that the invitation was created in the database
+ require Ash.Query
+
+ assert Helpcenter.Accounts.Invitation
+ |> Ash.Query.filter(email == ^params.email)
+ |> Ash.Query.filter(group_id == ^group.id)
+ |> Ash.Query.filter(status == :pending)
+ |> Ash.exists?(actor: user, authorize?: false)
+ end
+ end
+end
diff --git a/lib/helpcenter_web/live/accounts/users/user_invitations_live_test.exs b/lib/helpcenter_web/live/accounts/users/user_invitations_live_test.exs
new file mode 100644
index 0000000..ea509c8
--- /dev/null
+++ b/lib/helpcenter_web/live/accounts/users/user_invitations_live_test.exs
@@ -0,0 +1,17 @@
+defmodule HelpcenterWeb.Accounts.Users.UserInvitationsLiveTest do
+ use HelpcenterWeb.ConnCase
+
+ describe "User Invitations Live test" do
+ test "User can list and accept invitation", %{conn: conn} do
+ user = create_user()
+
+ conn =
+ conn
+ |> login(user)
+ |> get(~p"/accounts/users/invitations")
+
+ assert html_response(conn, 200) =~ "Email"
+ assert html_response(conn, 200) =~ "Team"
+ end
+ end
+end
diff --git a/lib/helpcenter_web/live/accounts/users/users_live.ex b/lib/helpcenter_web/live/accounts/users/users_live.ex
new file mode 100644
index 0000000..581ec85
--- /dev/null
+++ b/lib/helpcenter_web/live/accounts/users/users_live.ex
@@ -0,0 +1,32 @@
+# lib/helpcenter_web/live/accounts/users/users_live.ex
+defmodule HelpcenterWeb.Accounts.Users.UsersLive do
+ use HelpcenterWeb, :live_view
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+ <%!-- User list --%>
+
+ <:col :let={row} label="Email" field="email" filter sort>{row.email}
+ <:col :let={row} label="Team">{Phoenix.Naming.humanize(row.current_team)}
+
+ """
+ end
+
+ @impl true
+ def mount(_params, _session, socket) do
+ socket
+ |> assign(:page_title, "Users")
+ |> ok()
+ end
+
+ defp get_query(current_user) do
+ require Ash.Query
+
+ Helpcenter.Accounts.User
+ |> Ash.Query.filter(teams.domain == ^current_user.current_team)
+ |> Ash.Query.for_read(:read, %{}, authorize?: false)
+ end
+end
diff --git a/lib/helpcenter_web/live/accounts/users/users_live_test.exs b/lib/helpcenter_web/live/accounts/users/users_live_test.exs
new file mode 100644
index 0000000..28f6c52
--- /dev/null
+++ b/lib/helpcenter_web/live/accounts/users/users_live_test.exs
@@ -0,0 +1,19 @@
+# lib/helpcenter_web/live/accounts/users/users_live_test.exs
+defmodule HelpcenterWeb.Accounts.Users.UsersLiveTest do
+ use HelpcenterWeb.ConnCase
+
+ describe "UsersLive" do
+ test "renders the users page", %{conn: conn} do
+ user = create_user()
+
+ conn =
+ conn
+ |> login(user)
+ |> get(~p"/accounts/users")
+
+ assert html_response(conn, 200) =~ "Users"
+ assert html_response(conn, 200) =~ "Email"
+ assert html_response(conn, 200) =~ "Team"
+ end
+ end
+end
diff --git a/lib/helpcenter_web/router.ex b/lib/helpcenter_web/router.ex
index 7be5a1b..c671f8a 100644
--- a/lib/helpcenter_web/router.ex
+++ b/lib/helpcenter_web/router.ex
@@ -37,6 +37,8 @@ defmodule HelpcenterWeb.Router do
# If an authenticated user must *not* be present:
# on_mount {HelpcenterWeb.LiveUserAuth, :live_no_user}
+ live "/grok", GrokLayoutLive
+
scope "/categories" do
live "/", CategoriesLive
live "/create", CreateCategoryLive
@@ -44,9 +46,16 @@ defmodule HelpcenterWeb.Router do
end
# lib/helpcenter_web/router.ex
- scope "/accounts/groups", Accounts.Groups do
- live "/", GroupsLive
- live "/:group_id/permissions", GroupPermissionsLive
+ scope "/accounts", Accounts do
+ scope "/users", Users do
+ live "/", UsersLive
+ live "/invitations", UserInvitationsLive
+ end
+
+ scope "/groups", Groups do
+ live "/", GroupsLive
+ live "/:group_id/permissions", GroupPermissionsLive
+ end
end
end
end
@@ -79,6 +88,14 @@ defmodule HelpcenterWeb.Router do
pipe_through :browser
get "/", PageController, :home
+
+ get "/accounts/users/invitations/:tenant/:token/accept",
+ TeamInvitationAcceptanceController,
+ :accept
+
+ get "/accounts/users/invitations/:tenant/:token/reject",
+ TeamInvitationAcceptanceController,
+ :reject
end
# Other scopes may use custom stacks.
diff --git a/lib/test_helper.exs b/lib/test_helper.exs
new file mode 100644
index 0000000..92102e8
--- /dev/null
+++ b/lib/test_helper.exs
@@ -0,0 +1,2 @@
+ExUnit.start()
+Ecto.Adapters.SQL.Sandbox.mode(Helpcenter.Repo, :manual)
diff --git a/mix.exs b/mix.exs
index 1ddd017..1733be1 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,3 +1,4 @@
+# mix.exs
defmodule Helpcenter.MixProject do
use Mix.Project
@@ -9,6 +10,8 @@ defmodule Helpcenter.MixProject do
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
consolidate_protocols: Mix.env() != :dev,
+ # Include "lib" for integration tests
+ test_paths: ["test", "lib"],
aliases: aliases(),
deps: deps()
]
@@ -33,6 +36,7 @@ defmodule Helpcenter.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
+ {:cinder, "~> 0.5"},
{:gen_smtp, "~> 1.0"},
{:oban, "~> 2.0"},
{:ash_oban, "~> 0.4"},
@@ -40,7 +44,6 @@ defmodule Helpcenter.MixProject do
{:ash_authentication, "~> 4.0"},
{:ash_authentication_phoenix, "~> 2.0"},
{:picosat_elixir, "~> 0.2"},
- {:mix_test_watch, "~> 1.0"},
{:distillery, "~> 2.0"},
{:edeliver, "~> 1.0"},
{:ash_phoenix, "~> 2.0"},
diff --git a/mix.lock b/mix.lock
index 63d2fa2..5eb67b5 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,81 +1,82 @@
%{
"artificery": {:hex, :artificery, "0.4.3", "0bc4260f988dcb9dda4b23f9fc3c6c8b99a6220a331534fdf5bf2fd0d4333b02", [:mix], [], "hexpm", "12e95333a30e20884e937abdbefa3e7f5e05609c2ba8cf37b33f000b9ffc0504"},
- "ash": {:hex, :ash, "3.4.71", "ce8fa3c38bb59d067647bdc87aa9198335fdeeab36660c869b72c47339fc9d69", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.24 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f255da731b5b3ec4d916b5282faecbbbe9beb64a3641b4a45ac91160ffea3cc9"},
- "ash_authentication": {:hex, :ash_authentication, "4.6.0", "f466524b89166b76ec9847dc89283085119420d20bccaca6b52724081cb63911", [:mix], [{:ash, ">= 3.4.29 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, "~> 2.0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "~> 0.2.13", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "6c369d50fc8c702403ceedc329552ac631e3a83e5e0677b4901d1b77bbdbdb30"},
- "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.5.1", "aa11c8d58f26cd85e1de331d7e4423af2cbdfa9c35f1b5ec2c115a6badeb30d6", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.1", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "45980793197482cbcb3b76a5e60c87901ebc35f55bcc5c3399ec4cb44c605c88"},
+ "ash": {:hex, :ash, "3.5.25", "99f7139e98b745a64312ae80e2420589205b2fec1799f00fc58da771d2c63373", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.4 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.2.65 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d45844ea30062b796d4adcad75b8d91e21081ac0f1bb6627d1a2663ca5ecf258"},
+ "ash_authentication": {:hex, :ash_authentication, "4.9.5", "9a72fef7dc6912ef46bde34dc582eaa83c12e289c68b69456e8870857d122c85", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, ">= 3.4.29 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "~> 0.2.13", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "80e83db132c9b97fac739805f634f650cf8a30a904d6bb055be82c42b0506d29"},
+ "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.10.2", "474b450ca85e4773d0eafd5c1c9dbcfdfc6e95f496aa049d3c20d4cb701829d9", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, ">= 4.9.1 and < 5.0.0-0", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "d122d97b5269d8f2acba749312ea9c6574c03b647cac9ff501cecaf7f0af7592"},
"ash_oban": {:hex, :ash_oban, "0.4.9", "91873f1e2c9d462554ec4902e1a90378f4351277dd81f4371c0984aed4ee300e", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.18", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "f2d2ad468e0983f7a330f1511530d244209389a28e10bd0a05a0e30c7da6eae1"},
- "ash_phoenix": {:hex, :ash_phoenix, "2.1.23", "75c5500142d44c07431fcf7473784e6eed8d32777b68616de30b2ee7c3909110", [:mix], [{:ash, ">= 3.4.31 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, ">= 0.4.3 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "2d640dbd57020102d1a8997ab4b42035418696ebacfa5f8cbb94466e67deb8cf"},
- "ash_postgres": {:hex, :ash_postgres, "2.5.12", "cca37fb0a72114ea899f9a80cf7385b1826872263c75a7735f01898f4ff68a23", [:mix], [{:ash, ">= 3.4.69 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.62 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, ">= 3.12.1 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "b69a116b3d5b57fe868da914e1ed15286c46a0865b45f14950a38a6af06a915f"},
- "ash_sql": {:hex, :ash_sql, "0.2.62", "fcf1dde5a453cb024799bd43ab25aee3a7cc4ce7a48f1456310a65aec9e7ea7a", [:mix], [{:ash, ">= 3.4.65 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "df8c72b9b1c7b2c3147334eb63e819bc8d15288e1c6f0ddcd7691530db272ce0"},
+ "ash_phoenix": {:hex, :ash_phoenix, "2.3.9", "684645f02725ca71625fcade6a4cc7c3a881a150762cdc532d03a32bde5a366d", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "462ed487e62aa7de14d587c3fed64c9a3971e4132a9d0121033754c444c1400e"},
+ "ash_postgres": {:hex, :ash_postgres, "2.6.9", "8312bbe1ec463036841f08861196595ea233eadc1cd4c8097a1701ff7b0e95ed", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.72 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "1eb0c258d7dbe594312b37697097745ebb396162f38cddde282e011c26eb834c"},
+ "ash_sql": {:hex, :ash_sql, "0.2.84", "1187555609f4773aacb5cccdca82a78c2b3f7390e78b400a8f03c91b2e7cd82f", [:mix], [{:ash, ">= 3.5.25 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "5e6a4d3070e60a0653c572527276a8c034b9458e37b1aca8868b17fcf0a1d1c0"},
"assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"},
- "bandit": {:hex, :bandit, "1.6.9", "cf4653d0490941629a4475381eda3b8d4d2653471a9efe0147b2195bef40ece5", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "67ab91338f308da9fb10d5afde35899e15af653adf31d682dd3a0e7c1d34db23"},
- "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.1", "e361261a0401d82dadc1ab7b969f91d250bf7577283e933fe8c5b72f8f5b3c46", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "81170177d5c2e280d12141a0b9d9e299bf731535e2d959982bdcd4cfe3c82865"},
- "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"},
+ "bandit": {:hex, :bandit, "1.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"},
+ "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
+ "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"},
+ "cinder": {:hex, :cinder, "0.5.5", "745c7085694114777bf20a811d8213f103fab06388c9b8fa378479af238aaa36", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.3", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "c6dd26cb2e770b5742523defb15e1f8fec0e5261be9248e531f7f59f1e67ba06"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
- "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
+ "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"distillery": {:hex, :distillery, "2.1.1", "f9332afc2eec8a1a2b86f22429e068ef35f84a93ea1718265e740d90dd367814", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm", "bbc7008b0161a6f130d8d903b5b3232351fccc9c31a991f8fcbf2a12ace22995"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
- "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
- "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
+ "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"},
+ "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"},
"edeliver": {:hex, :edeliver, "1.9.2", "78caa5a261c265dc9c17226e890b330165612145da312012dbe35d0e8631291b", [:mix], [{:distillery, "~> 2.1.0", [hex: :distillery, repo: "hexpm", optional: true]}], "hexpm", "cf6600ec1326d8aa224410e2ee02133f88e4f71dedb70db7b98b6acaea7e771f"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
- "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"},
+ "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
- "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
- "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"},
+ "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
+ "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"},
"gen_smtp": {:hex, :gen_smtp, "1.3.0", "62c3d91f0dcf6ce9db71bcb6881d7ad0d1d834c7f38c13fa8e952f4104a8442e", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6"},
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
- "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
- "igniter": {:hex, :igniter, "0.5.38", "436a6414abc9245e539d6c92a6f4854f62270fbf6547c4acf1e5a65c0e4f4d4b", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "e5f1474a2a7ad186f3b71074d9d1ef25d306634e12af4ea01beae06ba958491d"},
+ "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
+ "igniter": {:hex, :igniter, "0.6.11", "bd7269949df4e668fba4219aad32fc922e48f5197e2e27e106cc44d972e50649", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "8dc87860a66597dd93abfaf640a3ce747bfcdf53d7dcbb2ec51f5edfb741307d"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
- "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
+ "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
- "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"},
+ "mix_test_watch": {:hex, :mix_test_watch, "1.3.0", "2ffc9f72b0d1f4ecf0ce97b044e0e3c607c3b4dc21d6228365e8bc7c2856dc77", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "f9e5edca976857ffac78632e635750d158df14ee2d6185a15013844af7570ffe"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"},
"owl": {:hex, :owl, "0.12.2", "65906b525e5c3ef51bab6cba7687152be017aebe1da077bb719a5ee9f7e60762", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "6398efa9e1fea70a04d24231e10dcd66c1ac1aa2da418d20ef5357ec61de2880"},
- "phoenix": {:hex, :phoenix, "1.7.20", "6bababaf27d59f5628f9b608de902a021be2cecefb8231e1dbdc0a2e2e480e9b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6be2ab98302e8784a31829e0d50d8bdfa81a23cd912c395bafd8b8bfb5a086c2"},
- "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"},
+ "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"},
"phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
"phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
- "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"},
- "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
- "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.7", "491c5fcccb9cee4978a25f0ec4c4b01975cd5f8d6d2366ca1bd5bf6f7f81a862", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a1758c5816f65c83af38dfeef35a6d491363e32c707c2e3bb6b8f6339e8f2cbf"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
- "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
- "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
+ "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
+ "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
- "reactor": {:hex, :reactor, "0.15.0", "556937d9310e1a6dd06083592b9eb9e0d212540b6d82faecba70823ee7a0747d", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "f634383a7760ba3106d31a3185f2e2c39e1485d899d884d94c22c62c9b5e7a4a"},
- "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"},
+ "reactor": {:hex, :reactor, "0.15.6", "d717f9add549b25a089a94c90197718d2d838e35d81dd776b1d81587d4cf2aaa", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "74db98165e3644d86e0f723672d91ceca4339eaa935bcad7e78bf146a46d77b9"},
+ "req": {:hex, :req, "0.5.14", "521b449fa0bf275e6d034c05f29bec21789a0d6cd6f7a1c326c7bee642bf6e07", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b7b15692071d556c73432c7797aa7e96b51d1a2db76f746b976edef95c930021"},
"rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"},
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
- "sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"},
- "spark": {:hex, :spark, "2.2.48", "dd1005c26c7f98ea686a951f7ae58fffb54eff19c47830e6ff68b93f87433baa", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "379912647b9ddcc5265e91a82a235a264a727123d1f9e90052d91ad8cebbb2d0"},
- "spitfire": {:hex, :spitfire, "0.2.0", "0de1f519a23f65bde40d316adad53c07a9563f25cc68915d639d8a509a0aad8a", [:mix], [], "hexpm", "743daaee2d81a0d8095431729f478ce49b47ea8943c7d770de86704975cb7775"},
+ "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"},
+ "spark": {:hex, :spark, "2.2.67", "67626cb9f59ea4b1c5aa85d4afdd025e0740cbd49ed82665d0a40ff007d7fd4b", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "c8575402e3afc66871362e821bece890536d16319cdb758c5fb2d1250182e46f"},
+ "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"},
"splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"},
- "stream_data": {:hex, :stream_data, "1.1.3", "15fdb14c64e84437901258bb56fc7d80aaf6ceaf85b9324f359e219241353bfb", [:mix], [], "hexpm", "859eb2be72d74be26c1c4f272905667672a52e44f743839c57c7ee73a1a66420"},
- "swoosh": {:hex, :swoosh, "1.18.3", "ca12197550bd7456654179055b1446168cc0f55067f784a3707e0e4462e269f5", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a533daccea84e887a061a919295212b37f4f2c7916436037eb8be7f1265bacba"},
+ "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
+ "swoosh": {:hex, :swoosh, "1.19.3", "02ad4455939f502386e4e1443d4de94c514995fd0e51b3cafffd6bd270ffe81c", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "04a10f8496786b744b84130e3510eb53ca51e769c39511b65023bdf4136b732f"},
"tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
- "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"},
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
- "thousand_island": {:hex, :thousand_island, "1.3.12", "590ff651a6d2a59ed7eabea398021749bdc664e2da33e0355e6c64e7e1a2ef93", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "55d0b1c868b513a7225892b8a8af0234d7c8981a51b0740369f3125f7c99a549"},
+ "thousand_island": {:hex, :thousand_island, "1.3.14", "ad45ebed2577b5437582bcc79c5eccd1e2a8c326abf6a3464ab6c06e2055a34a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d24a929d31cdd1d7903a4fe7f2409afeedff092d277be604966cd6aa4307ef"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
diff --git a/priv/repo/migrations/20250814182210_add_team_to_invitations.exs b/priv/repo/migrations/20250814182210_add_team_to_invitations.exs
new file mode 100644
index 0000000..ca1b4c5
--- /dev/null
+++ b/priv/repo/migrations/20250814182210_add_team_to_invitations.exs
@@ -0,0 +1,17 @@
+defmodule Helpcenter.Repo.Migrations.AddTeamToInvitations do
+ @moduledoc """
+ Updates resources based on their most recent snapshots.
+
+ This file was autogenerated with `mix ash_postgres.generate_migrations`
+ """
+
+ use Ecto.Migration
+
+ def up do
+ create unique_index(:teams, [:domain], name: "teams_unique_domain_index")
+ end
+
+ def down do
+ drop_if_exists unique_index(:teams, [:domain], name: "teams_unique_domain_index")
+ end
+end
diff --git a/priv/repo/migrations/20250829202959_add_ash_parental.exs b/priv/repo/migrations/20250829202959_add_ash_parental.exs
new file mode 100644
index 0000000..2ce5af5
--- /dev/null
+++ b/priv/repo/migrations/20250829202959_add_ash_parental.exs
@@ -0,0 +1,57 @@
+defmodule Helpcenter.Repo.Migrations.AddAshParental do
+ @moduledoc """
+ Updates resources based on their most recent snapshots.
+
+ This file was autogenerated with `mix ash_postgres.generate_migrations`
+ """
+
+ use Ecto.Migration
+
+ def up do
+ create table(:invitations, primary_key: false) do
+ add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
+ add :email, :text, null: false
+ add :status, :text, null: false, default: "pending"
+ add :token, :text, null: false
+ add :team, :text, null: false
+ add :expires_at, :utc_datetime, null: false
+
+ add :inserted_at, :utc_datetime_usec,
+ null: false,
+ default: fragment("(now() AT TIME ZONE 'utc')")
+
+ add :updated_at, :utc_datetime_usec,
+ null: false,
+ default: fragment("(now() AT TIME ZONE 'utc')")
+
+ add :group_id, :uuid, null: false
+
+ add :inviter_user_id,
+ references(:users,
+ column: :id,
+ name: "invitations_inviter_user_id_fkey",
+ type: :uuid,
+ prefix: "public"
+ ),
+ null: false
+
+ add :invitee_user_id,
+ references(:users,
+ column: :id,
+ name: "invitations_invitee_user_id_fkey",
+ type: :uuid,
+ prefix: "public"
+ )
+ end
+ end
+
+ def down do
+ drop constraint(:invitations, "invitations_group_id_fkey")
+
+ drop constraint(:invitations, "invitations_inviter_user_id_fkey")
+
+ drop constraint(:invitations, "invitations_invitee_user_id_fkey")
+
+ drop table(:invitations)
+ end
+end
diff --git a/priv/repo/tenant_migrations/20250814182208_add_team_to_invitations.exs b/priv/repo/tenant_migrations/20250814182208_add_team_to_invitations.exs
new file mode 100644
index 0000000..57d2ab4
--- /dev/null
+++ b/priv/repo/tenant_migrations/20250814182208_add_team_to_invitations.exs
@@ -0,0 +1,67 @@
+defmodule Helpcenter.Repo.TenantMigrations.AddTeamToInvitations do
+ @moduledoc """
+ Updates resources based on their most recent snapshots.
+
+ This file was autogenerated with `mix ash_postgres.generate_migrations`
+ """
+
+ use Ecto.Migration
+
+ def up do
+
+ create table(:invitations, primary_key: false, prefix: prefix()) do
+ add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
+ add :email, :text, null: false
+ add :status, :text, null: false, default: "pending"
+ add :token, :text, null: false
+ add :team, :text, null: false
+ add :expires_at, :utc_datetime, null: false
+
+ add :inserted_at, :utc_datetime_usec,
+ null: false,
+ default: fragment("(now() AT TIME ZONE 'utc')")
+
+ add :updated_at, :utc_datetime_usec,
+ null: false,
+ default: fragment("(now() AT TIME ZONE 'utc')")
+
+ add :group_id,
+ references(:groups,
+ column: :id,
+ name: "invitations_group_id_fkey",
+ type: :uuid,
+ prefix: prefix()
+ ),
+ null: false
+
+ add :inviter_user_id,
+ references(:users,
+ column: :id,
+ name: "invitations_inviter_user_id_fkey",
+ type: :uuid,
+ prefix: "public"
+ ),
+ null: false
+
+ add :invitee_user_id,
+ references(:users,
+ column: :id,
+ name: "invitations_invitee_user_id_fkey",
+ type: :uuid,
+ prefix: "public"
+ )
+ end
+ end
+
+ def down do
+ drop constraint(:invitations, "invitations_group_id_fkey")
+
+ drop constraint(:invitations, "invitations_inviter_user_id_fkey")
+
+ drop constraint(:invitations, "invitations_invitee_user_id_fkey")
+
+ drop table(:invitations, prefix: prefix())
+
+ drop_if_exists index(:user_notifications, [:recipient_user_id])
+ end
+end
diff --git a/priv/repo/tenant_migrations/20250829202957_add_ash_parental.exs b/priv/repo/tenant_migrations/20250829202957_add_ash_parental.exs
new file mode 100644
index 0000000..7ce6851
--- /dev/null
+++ b/priv/repo/tenant_migrations/20250829202957_add_ash_parental.exs
@@ -0,0 +1,30 @@
+defmodule Helpcenter.Repo.TenantMigrations.AddAshParental do
+ @moduledoc """
+ Updates resources based on their most recent snapshots.
+
+ This file was autogenerated with `mix ash_postgres.generate_migrations`
+ """
+
+ use Ecto.Migration
+
+ def up do
+ alter table(:comments, prefix: prefix()) do
+ add :parent_id,
+ references(:comments,
+ column: :id,
+ name: "comments_parent_id_fkey",
+ type: :uuid,
+ prefix: prefix()
+ ),
+ null: false
+ end
+ end
+
+ def down do
+ drop constraint(:comments, "comments_parent_id_fkey")
+
+ alter table(:comments, prefix: prefix()) do
+ remove :parent_id
+ end
+ end
+end
diff --git a/priv/repo/tenant_migrations/20250901141147_add_children_to_categories.exs b/priv/repo/tenant_migrations/20250901141147_add_children_to_categories.exs
new file mode 100644
index 0000000..a82f8fb
--- /dev/null
+++ b/priv/repo/tenant_migrations/20250901141147_add_children_to_categories.exs
@@ -0,0 +1,21 @@
+defmodule Helpcenter.Repo.TenantMigrations.AddChildrenToCategories do
+ @moduledoc """
+ Updates resources based on their most recent snapshots.
+
+ This file was autogenerated with `mix ash_postgres.generate_migrations`
+ """
+
+ use Ecto.Migration
+
+ def up do
+ alter table(:categories, prefix: prefix()) do
+ add :parent_id, :uuid
+ end
+ end
+
+ def down do
+ alter table(:categories, prefix: prefix()) do
+ remove :parent_id
+ end
+ end
+end
diff --git a/priv/resource_snapshots/repo/invitations/20250829202959.json b/priv/resource_snapshots/repo/invitations/20250829202959.json
new file mode 100644
index 0000000..cb42c19
--- /dev/null
+++ b/priv/resource_snapshots/repo/invitations/20250829202959.json
@@ -0,0 +1,208 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"gen_random_uuid()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "email",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "\"pending\"",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "status",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "token",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "team",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "expires_at",
+ "type": "utc_datetime"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "inserted_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "updated_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": false,
+ "strategy": "context"
+ },
+ "name": "invitations_group_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "groups"
+ },
+ "scale": null,
+ "size": null,
+ "source": "group_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "invitations_inviter_user_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "users"
+ },
+ "scale": null,
+ "size": null,
+ "source": "inviter_user_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "invitations_invitee_user_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "users"
+ },
+ "scale": null,
+ "size": null,
+ "source": "invitee_user_id",
+ "type": "uuid"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "62A4CE516E55B26E46A1694CB732E668884347ECD19B5EF2777D47F62DF6BC71",
+ "identities": [],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Helpcenter.Repo",
+ "schema": null,
+ "table": "invitations"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/teams/20250814182210.json b/priv/resource_snapshots/repo/teams/20250814182210.json
new file mode 100644
index 0000000..3a97d4f
--- /dev/null
+++ b/priv/resource_snapshots/repo/teams/20250814182210.json
@@ -0,0 +1,137 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"uuid_generate_v7()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "domain",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "description",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "inserted_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "updated_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "teams_owner_user_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "users"
+ },
+ "scale": null,
+ "size": null,
+ "source": "owner_user_id",
+ "type": "uuid"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "276437960B05C0DA3BCAFD936B7AA75EB9958B99F4353F40E141B81832E6B671",
+ "identities": [
+ {
+ "all_tenants?": false,
+ "base_filter": null,
+ "index_name": "teams_unique_domain_index",
+ "keys": [
+ {
+ "type": "atom",
+ "value": "domain"
+ }
+ ],
+ "name": "unique_domain",
+ "nils_distinct?": true,
+ "where": null
+ }
+ ],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Helpcenter.Repo",
+ "schema": null,
+ "table": "teams"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/tenants/categories/20250901141147.json b/priv/resource_snapshots/repo/tenants/categories/20250901141147.json
new file mode 100644
index 0000000..76aa59c
--- /dev/null
+++ b/priv/resource_snapshots/repo/tenants/categories/20250901141147.json
@@ -0,0 +1,103 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"gen_random_uuid()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "slug",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "description",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "parent_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "inserted_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "updated_at",
+ "type": "utc_datetime_usec"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "637C2B6D0D87B21171304593DB5D971408BE94E3AAE2EA361A8E21E2BE35F355",
+ "identities": [],
+ "multitenancy": {
+ "attribute": null,
+ "global": false,
+ "strategy": "context"
+ },
+ "repo": "Elixir.Helpcenter.Repo",
+ "schema": null,
+ "table": "categories"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/tenants/comments/20250829202957.json b/priv/resource_snapshots/repo/tenants/comments/20250829202957.json
new file mode 100644
index 0000000..5643bc0
--- /dev/null
+++ b/priv/resource_snapshots/repo/tenants/comments/20250829202957.json
@@ -0,0 +1,129 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"gen_random_uuid()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "content",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "inserted_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "updated_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": false,
+ "strategy": "context"
+ },
+ "name": "comments_article_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "articles"
+ },
+ "scale": null,
+ "size": null,
+ "source": "article_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": false,
+ "strategy": "context"
+ },
+ "name": "comments_parent_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "comments"
+ },
+ "scale": null,
+ "size": null,
+ "source": "parent_id",
+ "type": "uuid"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "963C70D2E954AA318FC603E9284CD0B91FFA68AE2AA4277FEC18E623F9904FFB",
+ "identities": [],
+ "multitenancy": {
+ "attribute": null,
+ "global": false,
+ "strategy": "context"
+ },
+ "repo": "Elixir.Helpcenter.Repo",
+ "schema": null,
+ "table": "comments"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/tenants/invitations/20250814182209.json b/priv/resource_snapshots/repo/tenants/invitations/20250814182209.json
new file mode 100644
index 0000000..10e6cb2
--- /dev/null
+++ b/priv/resource_snapshots/repo/tenants/invitations/20250814182209.json
@@ -0,0 +1,208 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"gen_random_uuid()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "email",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "\"pending\"",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "status",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "token",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "team",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "expires_at",
+ "type": "utc_datetime"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "inserted_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "updated_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": false,
+ "strategy": "context"
+ },
+ "name": "invitations_group_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "groups"
+ },
+ "scale": null,
+ "size": null,
+ "source": "group_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "invitations_inviter_user_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "users"
+ },
+ "scale": null,
+ "size": null,
+ "source": "inviter_user_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "invitations_invitee_user_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "users"
+ },
+ "scale": null,
+ "size": null,
+ "source": "invitee_user_id",
+ "type": "uuid"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "C943D079A2BEE09A33514945F44D9DF5DBA8DB94C5BC2C180E3A6DC9644C7ADE",
+ "identities": [],
+ "multitenancy": {
+ "attribute": null,
+ "global": false,
+ "strategy": "context"
+ },
+ "repo": "Elixir.Helpcenter.Repo",
+ "schema": null,
+ "table": "invitations"
+}
\ No newline at end of file
diff --git a/priv/resource_snapshots/repo/tenants/user_notifications/20250814182209.json b/priv/resource_snapshots/repo/tenants/user_notifications/20250814182209.json
new file mode 100644
index 0000000..98b3a8c
--- /dev/null
+++ b/priv/resource_snapshots/repo/tenants/user_notifications/20250814182209.json
@@ -0,0 +1,158 @@
+{
+ "attributes": [
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"gen_random_uuid()\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": true,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "sender_user_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": {
+ "deferrable": false,
+ "destination_attribute": "id",
+ "destination_attribute_default": null,
+ "destination_attribute_generated": null,
+ "index?": false,
+ "match_type": null,
+ "match_with": null,
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "name": "user_notifications_recipient_user_id_fkey",
+ "on_delete": null,
+ "on_update": null,
+ "primary_key?": true,
+ "schema": "public",
+ "table": "users"
+ },
+ "scale": null,
+ "size": null,
+ "source": "recipient_user_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "subject",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "body",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "read_at",
+ "type": "utc_datetime"
+ },
+ {
+ "allow_nil?": false,
+ "default": "\"unread\"",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "status",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "false",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "processed",
+ "type": "boolean"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "inserted_at",
+ "type": "utc_datetime_usec"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "updated_at",
+ "type": "utc_datetime_usec"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "6723A2BAC63EA63C7BD5D5F03417BD29E5E305FBE7F8238456A3CDD66C0D64CB",
+ "identities": [],
+ "multitenancy": {
+ "attribute": null,
+ "global": false,
+ "strategy": "context"
+ },
+ "repo": "Elixir.Helpcenter.Repo",
+ "schema": null,
+ "table": "user_notifications"
+}
\ No newline at end of file
diff --git a/test/helpcenter/accounts/group_test.exs b/test/helpcenter/accounts/group_test.exs
index 54d0689..47a73e2 100644
--- a/test/helpcenter/accounts/group_test.exs
+++ b/test/helpcenter/accounts/group_test.exs
@@ -18,7 +18,7 @@ defmodule Helpcenter.Accounts.GroupTest do
Helpcenter.Accounts.Group,
group_attrs,
actor: user,
- load: [:permissions, :users],
+ load: [:permissions],
authorize?: false
)
diff --git a/test/helpcenter/knowledge_base/category_test.exs b/test/helpcenter/knowledge_base/category_test.exs
index 89f0587..669c226 100644
--- a/test/helpcenter/knowledge_base/category_test.exs
+++ b/test/helpcenter/knowledge_base/category_test.exs
@@ -1,5 +1,6 @@
defmodule Helpcenter.KnowledgeBase.CategoryTest do
- use HelpcenterWeb.ConnCase, async: false
+ use HelpcenterWeb.ConnCase
+ import ArticleCase
import CategoryCase
require Ash.Query
@@ -67,160 +68,160 @@ defmodule Helpcenter.KnowledgeBase.CategoryTest do
|> Ash.exists?(actor: user)
end
- # test "Can destroy an existing Category" do
- # user = create_user()
-
- # create_categories(user.current_team)
-
- # require Ash.Query
-
- # # First identify category to destroy
- # category_to_delete =
- # Helpcenter.KnowledgeBase.Category
- # |> Ash.Query.filter(name == "Approvals and Workflows")
- # |> Ash.read_first!(actor: user)
-
- # # Tell Ash to destroy it
- # Ash.destroy(category_to_delete)
-
- # refute Helpcenter.KnowledgeBase.Category
- # |> Ash.Query.filter(name == "Approvals and Workflows")
- # |> Ash.exists?(actor: user)
- # end
-
- # test "Category can be created with an article" do
- # user = create_user()
-
- # # Define category and related article attributes
- # attrs = %{
- # name: "Features",
- # slug: "features",
- # description: "Category for features",
- # article_attrs: %{
- # title: "Compliance Features in Zippiker",
- # slug: "compliance-features-zippiker",
- # content: "Overview of compliance management features built into Zippiker."
- # }
- # }
-
- # # Create category and its article at the same time
- # Helpcenter.KnowledgeBase.Category
- # |> Ash.Changeset.for_create(:create_with_article, attrs, actor: user)
- # |> Ash.create()
-
- # assert Helpcenter.KnowledgeBase.Category
- # |> Ash.Query.filter(name == ^attrs.name)
- # |> Ash.exists?(actor: user)
-
- # assert Helpcenter.KnowledgeBase.Article
- # |> Ash.Query.filter(title == ^attrs.article_attrs.title)
- # |> Ash.exists?(actor: user)
- # end
-
- # test "An article can be added to an existing category" do
- # # 1. Get category to create an article under
- # user = create_user()
- # category = get_category(user.current_team)
-
- # # 2. Prepare new article data
- # attrs = %{
- # title: "Getting Started with Zippiker",
- # slug: "getting-started-zippiker",
- # content: "Learn how to set up your Zippiker account and configure basic settings.",
- # views_count: 1452,
- # published: true
- # }
-
- # # 3 Create an article under this category
- # {:ok, _category} =
- # category
- # |> Ash.Changeset.for_update(:add_article, %{article_attrs: attrs}, actor: user)
- # |> Ash.update()
-
- # # Confirm that the article has been create
- # assert Helpcenter.KnowledgeBase.Article
- # |> Ash.Query.filter(title == ^attrs.title)
- # |> Ash.Query.filter(content == ^attrs.content)
- # |> Ash.Query.filter(category_id == ^category.id)
- # |> Ash.read(actor: user)
- # end
-
- # test "Category can be retrieved with related articles" do
- # # First create articles for the category
- # user = create_user()
-
- # category = get_category(user.current_team)
- # articles = create_articles(category, user.current_team)
-
- # category_with_articles =
- # Helpcenter.KnowledgeBase.Category
- # |> Ash.Query.filter(id == ^category.id)
- # # Tell Ash to load related articles
- # |> Ash.Query.load(:articles)
- # |> Ash.read_first!(actor: user)
-
- # # This category might have added article else where in concurrency writing. Thus, use <=
- # assert Enum.count(category_with_articles.articles) <= Enum.count(articles)
- # end
-
- # test "articles_count aggregate can be loaded on the category" do
- # # Create categories and seed articles
- # user = create_user()
-
- # category = get_category(user.current_team)
- # create_articles(category, user.current_team)
-
- # loaded_category =
- # Helpcenter.KnowledgeBase.Category
- # |> Ash.Query.filter(id == ^category.id)
- # |> Ash.Query.load([:article_count, :articles])
- # |> Ash.read_first!(actor: user)
-
- # assert loaded_category.article_count == Enum.count(loaded_category.articles)
- # end
-
- # test "'categories' pubsub event is published on create" do
- # # Subscribe to the event so we can test whether it is being fired
- # user = create_user()
- # HelpcenterWeb.Endpoint.subscribe("categories")
-
- # attributes = %{name: "Art 1", slug: "art-1", description: "descrpt-1"}
-
- # Helpcenter.KnowledgeBase.Category
- # |> Ash.Changeset.for_create(:create, attributes, actor: user)
- # |> Ash.create()
-
- # # Confirm that the event is being recieved and its data
- # assert_receive %Phoenix.Socket.Broadcast{topic: "categories", payload: category}
- # assert category.data.name == attributes.name
- # assert category.data.slug == attributes.slug
- # assert category.data.description == attributes.description
- # end
-
- # test "Global preparations works as expected" do
- # user = create_user()
-
- # create_categories(user.current_team)
-
- # assert Helpcenter.KnowledgeBase.Category
- # |> Helpcenter.Preparations.LimitTo5.prepare([], [])
- # |> Helpcenter.Preparations.MonthToDate.prepare([], [])
- # |> Helpcenter.Preparations.OrderByMostRecent.prepare([], [])
- # |> Ash.count!(actor: user) == 5
- # end
-
- # test "Slug change generates slug successfully" do
- # user = create_user()
-
- # params = %{
- # name: "Home appliances you cannot find elsewhere",
- # description: "Home appliances description"
- # }
-
- # {:ok, category} =
- # Ash.create(Helpcenter.KnowledgeBase.Category, params, tenant: user.current_team)
-
- # refute category.slug |> is_nil()
- # end
+ test "Can destroy an existing Category" do
+ user = create_user()
+
+ create_categories(user.current_team)
+
+ require Ash.Query
+
+ # First identify category to destroy
+ category_to_delete =
+ Helpcenter.KnowledgeBase.Category
+ |> Ash.Query.filter(name == "Approvals and Workflows")
+ |> Ash.read_first!(actor: user)
+
+ # Tell Ash to destroy it
+ Ash.destroy(category_to_delete, actor: user)
+
+ refute Helpcenter.KnowledgeBase.Category
+ |> Ash.Query.filter(name == "Approvals and Workflows")
+ |> Ash.exists?(actor: user)
+ end
+
+ test "Category can be created with an article" do
+ user = create_user()
+
+ # Define category and related article attributes
+ attrs = %{
+ name: "Features",
+ slug: "features",
+ description: "Category for features",
+ article_attrs: %{
+ title: "Compliance Features in Zippiker",
+ slug: "compliance-features-zippiker",
+ content: "Overview of compliance management features built into Zippiker."
+ }
+ }
+
+ # Create category and its article at the same time
+ Helpcenter.KnowledgeBase.Category
+ |> Ash.Changeset.for_create(:create_with_article, attrs, actor: user)
+ |> Ash.create()
+
+ assert Helpcenter.KnowledgeBase.Category
+ |> Ash.Query.filter(name == ^attrs.name)
+ |> Ash.exists?(actor: user)
+
+ assert Helpcenter.KnowledgeBase.Article
+ |> Ash.Query.filter(title == ^attrs.article_attrs.title)
+ |> Ash.exists?(actor: user)
+ end
+
+ test "An article can be added to an existing category" do
+ # 1. Get category to create an article under
+ user = create_user()
+ category = get_category(user.current_team)
+
+ # 2. Prepare new article data
+ attrs = %{
+ title: "Getting Started with Zippiker",
+ slug: "getting-started-zippiker",
+ content: "Learn how to set up your Zippiker account and configure basic settings.",
+ views_count: 1452,
+ published: true
+ }
+
+ # 3 Create an article under this category
+ {:ok, _category} =
+ category
+ |> Ash.Changeset.for_update(:add_article, %{article_attrs: attrs}, actor: user)
+ |> Ash.update()
+
+ # Confirm that the article has been create
+ assert Helpcenter.KnowledgeBase.Article
+ |> Ash.Query.filter(title == ^attrs.title)
+ |> Ash.Query.filter(content == ^attrs.content)
+ |> Ash.Query.filter(category_id == ^category.id)
+ |> Ash.read(actor: user)
+ end
+
+ test "Category can be retrieved with related articles" do
+ # First create articles for the category
+ user = create_user()
+
+ category = get_category(user.current_team)
+ articles = create_articles(category, user.current_team)
+
+ category_with_articles =
+ Helpcenter.KnowledgeBase.Category
+ |> Ash.Query.filter(id == ^category.id)
+ # Tell Ash to load related articles
+ |> Ash.Query.load(:articles)
+ |> Ash.read_first!(actor: user)
+
+ # This category might have added article else where in concurrency writing. Thus, use <=
+ assert Enum.count(category_with_articles.articles) <= Enum.count(articles)
+ end
+
+ test "articles_count aggregate can be loaded on the category" do
+ # Create categories and seed articles
+ user = create_user()
+
+ category = get_category(user.current_team)
+ create_articles(category, user.current_team)
+
+ loaded_category =
+ Helpcenter.KnowledgeBase.Category
+ |> Ash.Query.filter(id == ^category.id)
+ |> Ash.Query.load([:article_count, :articles])
+ |> Ash.read_first!(actor: user)
+
+ assert loaded_category.article_count == Enum.count(loaded_category.articles)
+ end
+
+ test "'categories' pubsub event is published on create" do
+ # Subscribe to the event so we can test whether it is being fired
+ user = create_user()
+ HelpcenterWeb.Endpoint.subscribe("categories")
+
+ attributes = %{name: "Art 1", slug: "art-1", description: "descrpt-1"}
+
+ Helpcenter.KnowledgeBase.Category
+ |> Ash.Changeset.for_create(:create, attributes, actor: user)
+ |> Ash.create()
+
+ # Confirm that the event is being recieved and its data
+ assert_receive %Phoenix.Socket.Broadcast{topic: "categories", payload: category}
+ assert category.data.name == attributes.name
+ assert category.data.slug == attributes.slug
+ assert category.data.description == attributes.description
+ end
+
+ test "Global preparations works as expected" do
+ user = create_user()
+
+ create_categories(user.current_team)
+
+ assert Helpcenter.KnowledgeBase.Category
+ |> Helpcenter.Preparations.LimitTo5.prepare([], [])
+ |> Helpcenter.Preparations.MonthToDate.prepare([], [])
+ |> Helpcenter.Preparations.OrderByMostRecent.prepare([], [])
+ |> Ash.count!(actor: user) == 5
+ end
+
+ test "Slug change generates slug successfully" do
+ user = create_user()
+
+ params = %{
+ name: "Home appliances you cannot find elsewhere",
+ description: "Home appliances description"
+ }
+
+ {:ok, category} =
+ Ash.create(Helpcenter.KnowledgeBase.Category, params, actor: user)
+
+ refute category.slug |> is_nil()
+ end
end
end