3.8 KiB · text History bfa2c00
defmodule DiogramosWeb.Api.V1.CanvasController do
  @moduledoc """
  Canvas metadata CRUD and exports.

  There is deliberately **no endpoint here that writes `document`**. Every
  document mutation goes through `DiogramosWeb.CanvasChannel` into
  `Diogramos.Diagrams.Authority`, which is the only thing allowed to
  sequence ops and bump `version`. A second HTTP write path into the same
  GenServer would have no ordering story.
  """
  use DiogramosWeb, :controller

  alias Diogramos.Diagrams
  alias Diogramos.Diagrams.{Authority, MermaidExport, Permissions}

  action_fallback DiogramosWeb.Api.FallbackController

  def index(conn, params) do
    scope = conn.assigns.current_scope

    canvases =
      case Map.fetch(params, "folder_id") do
        {:ok, ""} -> Diagrams.list_canvases_in_folder(scope, nil)
        {:ok, "none"} -> Diagrams.list_canvases_in_folder(scope, nil)
        {:ok, id} -> Diagrams.list_canvases_in_folder(scope, String.to_integer(id))
        :error -> Diagrams.list_canvases(scope)
      end

    render(conn, :index, canvases: canvases, roles: roles_for(scope, canvases))
  end

  def show(conn, %{"slug" => slug}) do
    scope = conn.assigns.current_scope

    with {:ok, canvas} <- fetch_canvas(scope, slug) do
      # Read through the authority, not the row: a canvas being actively
      # edited holds unpersisted ops in memory (it only snapshots every
      # 25), so the DB copy can be stale by up to 25 ops.
      {document, version} =
        case Authority.snapshot(canvas.id) do
          {:ok, %{document: doc, version: version}} -> {doc, version}
          {:error, _} -> {canvas.document, canvas.version}
        end

      canvas = %{canvas | document: document, version: version}
      render(conn, :show, canvas: canvas, role: Permissions.effective_role(scope, canvas))
    end
  end

  def create(conn, params) do
    scope = conn.assigns.current_scope

    with {:ok, canvas} <- Diagrams.create_canvas(scope, canvas_params(params)) do
      conn
      |> put_status(:created)
      |> render(:show, canvas: canvas, role: Permissions.effective_role(scope, canvas))
    end
  end

  def update(conn, %{"slug" => slug} = params) do
    scope = conn.assigns.current_scope

    with {:ok, canvas} <- fetch_canvas(scope, slug),
         {:ok, canvas} <- Diagrams.update_canvas_metadata(scope, canvas, canvas_params(params)) do
      render(conn, :show, canvas: canvas, role: Permissions.effective_role(scope, canvas))
    end
  end

  def delete(conn, %{"slug" => slug}) do
    scope = conn.assigns.current_scope

    with {:ok, canvas} <- fetch_canvas(scope, slug),
         {:ok, _canvas} <- Diagrams.delete_canvas(scope, canvas) do
      Authority.stop(canvas.id)
      send_resp(conn, :no_content, "")
    end
  end

  def export(conn, %{"slug" => slug}) do
    scope = conn.assigns.current_scope

    with {:ok, canvas} <- fetch_canvas(scope, slug),
         {:ok, payload} <- Diagrams.export_canvas(scope, canvas) do
      json(conn, payload)
    end
  end

  def export_mermaid(conn, %{"slug" => slug}) do
    scope = conn.assigns.current_scope

    with {:ok, canvas} <- fetch_canvas(scope, slug) do
      document =
        case Authority.snapshot(canvas.id) do
          {:ok, %{document: doc}} -> doc
          {:error, _} -> canvas.document
        end

      conn
      |> put_resp_content_type("text/plain")
      |> send_resp(200, MermaidExport.to_mermaid(document))
    end
  end

  ## Helpers

  defp fetch_canvas(scope, slug) do
    case Diagrams.get_canvas_by_slug(scope, slug) do
      nil -> {:error, :not_found}
      canvas -> {:ok, canvas}
    end
  end

  defp canvas_params(params) do
    Map.take(params, ["slug", "title", "theme", "folder_id", "embed_show_cursors"])
  end

  defp roles_for(scope, canvases) do
    Map.new(canvases, &{&1.id, Permissions.effective_role(scope, &1)})
  end
end