Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/sequin/consumers/consumers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ defmodule Sequin.Consumers do
|> Enum.reduce(Function.where_account_id(account_id), fn
{:name, name}, query -> Function.where_name(query, name)
{:id, id}, query -> Function.where_id(query, id)
{:id_or_name, id_or_name}, query -> Function.where_id_or_name(query, id_or_name)
end)
|> Repo.one()
|> case do
Expand Down Expand Up @@ -229,10 +230,16 @@ defmodule Sequin.Consumers do

def delete_function(account_id, id) do
with {:ok, function} <- get_function_for_account(account_id, id) do
# Every column on sink_consumers that references functions(id). Constraint
# names follow the original column names (transform_id predates the
# transforms→functions rename and kept its FK name), so list each one
# explicitly rather than guessing.
function
|> Function.changeset(%{})
|> Ecto.Changeset.foreign_key_constraint(:id, name: "sink_consumers_function_id_fkey")
|> Ecto.Changeset.foreign_key_constraint(:id, name: "sink_consumers_transform_id_fkey")
|> Ecto.Changeset.foreign_key_constraint(:id, name: "sink_consumers_routing_id_fkey")
|> Ecto.Changeset.foreign_key_constraint(:id, name: "sink_consumers_filter_id_fkey")
|> Ecto.Changeset.foreign_key_constraint(:id, name: "sink_consumers_enrichment_id_fkey")
|> Repo.delete()
end
end
Expand Down
31 changes: 31 additions & 0 deletions lib/sequin_web/controllers/function_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule SequinWeb.FunctionController do
use SequinWeb, :controller

alias Sequin.Consumers
alias SequinWeb.ApiFallbackPlug

action_fallback ApiFallbackPlug

def index(conn, _params) do
account_id = conn.assigns.account_id

render(conn, "index.json", functions: Consumers.list_functions_for_account(account_id))
end

def show(conn, %{"id_or_name" => id_or_name}) do
account_id = conn.assigns.account_id

with {:ok, function} <- Consumers.find_function(account_id, id_or_name: id_or_name) do
render(conn, "show.json", function: function)
end
end

def delete(conn, %{"id_or_name" => id_or_name}) do
account_id = conn.assigns.account_id

with {:ok, function} <- Consumers.find_function(account_id, id_or_name: id_or_name),
{:ok, _function} <- Consumers.delete_function(account_id, function.id) do
render(conn, "delete.json", function: function)
end
end
end
31 changes: 31 additions & 0 deletions lib/sequin_web/controllers/function_json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule SequinWeb.FunctionJSON do
@doc """
Renders a list of functions.
"""
def index(%{functions: functions}) do
%{data: for(function <- functions, do: render_one(function))}
end

@doc """
Renders a single function.
"""
def show(%{function: function}) do
render_one(function)
end

@doc """
Renders a deleted function.
"""
def delete(%{function: function}) do
%{id: function.id, deleted: true}
end

# `Sequin.Transforms.to_external/1` intentionally omits identity fields
# (it's shared with config export, which is ID-agnostic). The management
# API needs the id so clients can follow up with show/delete.
defp render_one(function) do
function
|> Sequin.Transforms.to_external()
|> Map.put(:id, function.id)
end
end
3 changes: 3 additions & 0 deletions lib/sequin_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ defmodule SequinWeb.Router do
# HTTP Endpoints routes
resources("/destinations/http_endpoints", HttpEndpointController, except: [:new, :edit], param: "id_or_name")

# Function routes
resources("/functions", FunctionController, only: [:index, :show, :delete], param: "id_or_name")

# Sink Consumer routes
resources("/sinks", SinkConsumerController, except: [:new, :edit], param: "id_or_name")
# Backfill routes
Expand Down
86 changes: 86 additions & 0 deletions test/sequin_web/controllers/function_controller_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
defmodule SequinWeb.FunctionControllerTest do
use SequinWeb.ConnCase, async: true

alias Sequin.Consumers
alias Sequin.Factory.AccountsFactory
alias Sequin.Factory.ConsumersFactory
alias Sequin.Factory.FunctionsFactory

setup :authenticated_conn

setup %{account: account} do
other_account = AccountsFactory.insert_account!()
function = FunctionsFactory.insert_function!(account_id: account.id)
other_function = FunctionsFactory.insert_function!(account_id: other_account.id)

%{function: function, other_function: other_function, other_account: other_account}
end

describe "index" do
test "lists functions in the given account", %{conn: conn, account: account, function: function} do
another_function = FunctionsFactory.insert_function!(account_id: account.id)

conn = get(conn, ~p"/api/functions")
assert %{"data" => functions} = json_response(conn, 200)
assert length(functions) == 2
ids = Enum.map(functions, & &1["id"])
assert function.id in ids
assert another_function.id in ids
end

test "does not list functions from another account", %{conn: conn, other_function: other_function} do
conn = get(conn, ~p"/api/functions")
assert %{"data" => functions} = json_response(conn, 200)
refute Enum.any?(functions, &(&1["id"] == other_function.id))
end
end

describe "show" do
test "shows function details by id", %{conn: conn, function: function} do
conn = get(conn, ~p"/api/functions/#{function.id}")
assert json = json_response(conn, 200)
assert json["name"] == function.name
end

test "shows function details by name", %{conn: conn, function: function} do
conn = get(conn, ~p"/api/functions/#{function.name}")
assert json = json_response(conn, 200)
assert json["name"] == function.name
end

test "returns 404 if function belongs to another account", %{conn: conn, other_function: other_function} do
conn = get(conn, ~p"/api/functions/#{other_function.id}")
assert json_response(conn, 404)
end
end

describe "delete" do
test "deletes the function", %{conn: conn, function: function} do
conn = delete(conn, ~p"/api/functions/#{function.id}")
assert %{"id" => id, "deleted" => true} = json_response(conn, 200)

assert {:error, _} = Consumers.find_function(function.account_id, id: id)
end

test "deletes by name", %{conn: conn, function: function} do
conn = delete(conn, ~p"/api/functions/#{function.name}")
assert %{"deleted" => true} = json_response(conn, 200)
end

test "returns 404 if function belongs to another account", %{conn: conn, other_function: other_function} do
conn = delete(conn, ~p"/api/functions/#{other_function.id}")
assert json_response(conn, 404)
end

test "refuses to delete a function still referenced by a sink", %{conn: conn, account: account, function: function} do
sink = ConsumersFactory.insert_sink_consumer!(account_id: account.id)
{:ok, _sink} = Consumers.update_sink_consumer(sink, %{transform_id: function.id})

conn = delete(conn, ~p"/api/functions/#{function.id}")
assert conn.status in 400..499

# function must still exist
assert {:ok, _} = Consumers.find_function(account.id, id: function.id)
end
end
end
Loading