feat: closing last gaps in backend functions we have available
This commit is contained in:
24
lib/bds/mcp/proposal.ex
Normal file
24
lib/bds/mcp/proposal.ex
Normal file
@@ -0,0 +1,24 @@
|
||||
defmodule BDS.MCP.Proposal do
|
||||
@moduledoc false
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@primary_key {:id, :string, autogenerate: false}
|
||||
|
||||
schema "mcp_proposals" do
|
||||
field :kind, :string
|
||||
field :status, Ecto.Enum, values: [:pending, :accepted, :discarded, :expired], default: :pending
|
||||
field :entity_id, :string
|
||||
field :data, :map
|
||||
field :created_at, :integer
|
||||
field :expires_at, :integer
|
||||
end
|
||||
|
||||
def changeset(proposal, attrs) do
|
||||
proposal
|
||||
|> cast(attrs, [:id, :kind, :status, :entity_id, :data, :created_at, :expires_at], empty_values: [nil])
|
||||
|> validate_required([:id, :kind, :status, :entity_id, :data, :created_at, :expires_at])
|
||||
|> unique_constraint(:status, name: :mcp_proposals_entity_idx)
|
||||
end
|
||||
end
|
||||
@@ -1,51 +1,45 @@
|
||||
defmodule BDS.MCP.ProposalStore do
|
||||
@moduledoc false
|
||||
|
||||
use Agent
|
||||
import Ecto.Query
|
||||
|
||||
alias BDS.MCP.Proposal
|
||||
alias BDS.Persistence
|
||||
alias BDS.Repo
|
||||
|
||||
@default_ttl_ms 30 * 60 * 1000
|
||||
|
||||
def ensure_started do
|
||||
case Process.whereis(__MODULE__) do
|
||||
nil ->
|
||||
case Agent.start_link(fn -> %{} end, name: __MODULE__) do
|
||||
{:ok, _pid} -> :ok
|
||||
{:error, {:already_started, _pid}} -> :ok
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
|
||||
_pid ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
def ensure_started, do: :ok
|
||||
|
||||
def create(kind, data, opts \\ []) when is_binary(kind) and is_map(data) do
|
||||
:ok = ensure_started()
|
||||
cleanup_expired(opts)
|
||||
now = Persistence.now_ms()
|
||||
|
||||
proposal = %{
|
||||
entity_id = Keyword.get(opts, :entity_id) || derive_entity_id(data)
|
||||
|
||||
%Proposal{}
|
||||
|> Proposal.changeset(%{
|
||||
id: Ecto.UUID.generate(),
|
||||
kind: kind,
|
||||
status: :pending,
|
||||
entity_id: entity_id,
|
||||
data: data,
|
||||
created_at: Persistence.now_ms(),
|
||||
expires_at: Persistence.now_ms() + Keyword.get(opts, :ttl_ms, @default_ttl_ms)
|
||||
}
|
||||
|
||||
Agent.update(__MODULE__, &Map.put(&1, proposal.id, proposal))
|
||||
proposal
|
||||
created_at: now,
|
||||
expires_at: now + Keyword.get(opts, :ttl_ms, @default_ttl_ms)
|
||||
})
|
||||
|> Repo.insert!()
|
||||
end
|
||||
|
||||
def get(id) when is_binary(id) do
|
||||
:ok = ensure_started()
|
||||
cleanup_expired([])
|
||||
Agent.get(__MODULE__, &Map.get(&1, id))
|
||||
Repo.get(Proposal, id)
|
||||
end
|
||||
|
||||
def remove(id) when is_binary(id) do
|
||||
:ok = ensure_started()
|
||||
Agent.update(__MODULE__, &Map.delete(&1, id))
|
||||
Repo.delete_all(from proposal in Proposal, where: proposal.id == ^id)
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -53,26 +47,49 @@ defmodule BDS.MCP.ProposalStore do
|
||||
:ok = ensure_started()
|
||||
cleanup_expired([])
|
||||
|
||||
Agent.get(__MODULE__, fn proposals ->
|
||||
proposals
|
||||
|> Map.values()
|
||||
|> Enum.sort_by(& &1.created_at)
|
||||
end)
|
||||
Repo.all(from proposal in Proposal, order_by: [asc: proposal.created_at])
|
||||
end
|
||||
|
||||
def cleanup_expired(opts) do
|
||||
def cleanup_expired(opts \\ []) do
|
||||
:ok = ensure_started()
|
||||
now = Persistence.now_ms()
|
||||
on_expire = Keyword.get(opts, :on_expire)
|
||||
|
||||
Agent.get_and_update(__MODULE__, fn proposals ->
|
||||
{expired, active} = Enum.split_with(proposals, fn {_id, proposal} -> proposal.expires_at <= now end)
|
||||
expired =
|
||||
Repo.all(
|
||||
from proposal in Proposal,
|
||||
where: proposal.status == :pending and proposal.expires_at <= ^now
|
||||
)
|
||||
|
||||
Enum.each(expired, fn {_id, proposal} ->
|
||||
if is_function(on_expire, 1), do: on_expire.(proposal)
|
||||
end)
|
||||
|
||||
{Enum.map(expired, &elem(&1, 1)), Map.new(active)}
|
||||
Enum.each(expired, fn proposal ->
|
||||
if is_function(on_expire, 1), do: on_expire.(proposal)
|
||||
mark_status(proposal.id, :expired)
|
||||
end)
|
||||
|
||||
Enum.map(expired, &Repo.get(Proposal, &1.id))
|
||||
end
|
||||
|
||||
def mark_accepted(id) when is_binary(id), do: mark_status(id, :accepted)
|
||||
def mark_discarded(id) when is_binary(id), do: mark_status(id, :discarded)
|
||||
|
||||
defp mark_status(id, status) do
|
||||
case Repo.get(Proposal, id) do
|
||||
nil -> nil
|
||||
proposal ->
|
||||
Repo.delete_all(
|
||||
from other in Proposal,
|
||||
where:
|
||||
other.id != ^id and other.kind == ^proposal.kind and other.entity_id == ^proposal.entity_id and
|
||||
other.status == ^status
|
||||
)
|
||||
|
||||
proposal
|
||||
|> Proposal.changeset(%{status: status})
|
||||
|> Repo.update!()
|
||||
end
|
||||
end
|
||||
|
||||
defp derive_entity_id(data) do
|
||||
data["post_id"] || data["script_id"] || data["template_id"] || data["media_id"] || Ecto.UUID.generate()
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user