Packages

phoenix_kit

2.48.0
2.60.3 2.60.2 2.60.1 2.60.0 2.59.0 2.58.0 2.57.1 2.57.0 2.56.1 2.56.0 2.55.1 2.55.0 2.54.2 2.54.1 2.54.0 2.53.0 2.52.2 2.52.1 2.52.0 2.51.0 2.50.0 2.49.1 2.49.0 2.48.0 2.47.0 2.46.0 2.45.0 2.44.0 2.43.1 2.43.0 2.42.1 2.42.0 2.41.6 2.41.4 2.41.3 2.41.2 2.41.1 2.41.0 2.40.1 2.40.0 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 2.37.2 2.37.1 2.37.0 2.36.1 2.36.0 2.35.0 2.34.0 2.33.0 2.32.1 2.32.0 2.31.1 2.31.0 2.30.0 2.29.1 2.29.0 2.28.2 2.28.1 2.28.0 2.27.2 2.27.1 2.27.0 2.26.1 2.26.0 2.25.0 2.24.0 2.23.3 2.23.2 2.23.1 2.23.0 2.22.24 2.22.23 2.22.22 2.22.21 2.22.20 2.22.19 2.22.18 2.22.17 2.22.16 2.22.15 2.22.14 2.22.13 2.22.12 2.22.11 2.22.10 2.22.9 2.22.8 2.22.7 2.22.6 2.22.5 2.22.4 2.22.3 2.22.2 2.22.1 2.22.0 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.0 2.19.0 2.18.1 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.2 2.14.1 2.14.0 2.13.19 2.13.18 2.13.17 2.13.16 2.13.15 2.13.13 2.13.12 2.13.11 2.13.10 2.13.9 2.13.8 2.13.7 2.13.6 2.13.5 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules storage etcher_adapter.ex
Raw

lib/modules/storage/etcher_adapter.ex

defmodule PhoenixKit.Modules.Storage.EtcherAdapter do
  @moduledoc """
  Persistence helper for the MediaBrowser's annotation flow.

  Etcher 0.3 dropped the `Etcher.Storage` behaviour entirely — annotations
  now live inside the host `<Fresco.canvas>`'s `extensions.etcher` blob
  and the library doesn't reach into the consumer's DB anymore. PhoenixKit
  still needs to persist its annotations (they're per-file, not per-canvas-
  file-on-disk), so this module survives as a thin helper module called
  from the MediaBrowser LV's `etcher:annotations-changed` event handler —
  not as a behaviour implementation.

  The four public functions (`create/1`, `list_for/2`, `update/2`,
  `delete/1`) keep their pre-0.3 signatures so the diff in MediaBrowser
  stays small. None of them are `@impl` annotations anymore; they're
  just plain helpers wrapping the `PhoenixKit.Annotations` context.

  Etcher's generic API is keyed by `target_type` + `target_uuid` so the
  library can annotate any kind of resource. A `"file"` target is a media
  File and is stored with `file_uuid` set as well (the hard FK the
  file-side features key on); any other target — a projects whiteboard,
  say — is stored by the pair alone, with no file (V183). Both shapes
  are pinned by the schema and a CHECK.

  ## Comment threads

  An annotation's discussion thread is **not** created at draw time —
  it's instantiated lazily when the user posts the first comment on the
  annotation. The comments are anchored to the **file**
  (`resource_type = "file"`, `resource_uuid = file_uuid`) with
  `metadata.annotation_uuid` carrying the back-reference, so they
  appear in the file's main thread alongside non-annotated discussion.
  No `comment_uuid` column on annotations is needed.
  """

  alias PhoenixKit.Annotations
  alias PhoenixKit.Annotations.Annotation

  # Whitelist of annotation schema fields the helper accepts from event
  # payloads, sourced from `Annotation.adapter_writable_fields/0` so the
  # set stays in sync with the schema's `@cast_fields`. Anything else
  # (Etcher routing keys, JS-side anchor coords, comment-derived metadata
  # we hydrate server-side) is silently dropped — `String.to_existing_atom`
  # on unknown payload keys used to crash the LV when Etcher's payload
  # shape grew new client-side keys. Stored as strings here since the
  # filter compares against `to_string(payload_key)`.
  @schema_keys Enum.map(Annotation.adapter_writable_fields(), &Atom.to_string/1)

  def create(attrs) do
    with {:ok, target_type, target_uuid} <- target(attrs) do
      attrs
      |> filter_to_schema()
      |> Map.put(:target_type, target_type)
      |> Map.put(:target_uuid, target_uuid)
      |> Map.put(:file_uuid, if(target_type == "file", do: target_uuid))
      |> Annotations.create()
    end
  end

  def list_for(target_type, target_uuid) when is_binary(target_type) and is_binary(target_uuid),
    do: Annotations.list_for_target(target_type, target_uuid)

  def list_for(_other, _uuid), do: []

  def update(uuid, attrs) do
    attrs
    |> filter_to_schema()
    |> then(&Annotations.update(uuid, &1))
  end

  def delete(uuid), do: Annotations.delete(uuid)

  # ---------------------------------------------------------------------------

  # The schema owns the shape; this is the early rejection the Etcher protocol
  # wants (`{:error, :unsupported_target}` rather than a changeset). Reading it
  # from `Annotation` keeps the two from drifting.

  defp target(%{"target_type" => type, "target_uuid" => uuid}), do: target(type, uuid)
  defp target(%{target_type: type, target_uuid: uuid}), do: target(type, uuid)
  defp target(_attrs), do: {:error, :unsupported_target}

  defp target(type, uuid) when is_binary(type) and is_binary(uuid) do
    if Regex.match?(Annotation.target_type_format(), type),
      do: {:ok, type, uuid},
      else: {:error, :unsupported_target}
  end

  defp target(_type, _uuid), do: {:error, :unsupported_target}

  defp filter_to_schema(attrs) do
    Enum.reduce(attrs, %{}, fn {k, v}, acc ->
      key = to_string(k)
      if key in @schema_keys, do: Map.put(acc, String.to_existing_atom(key), v), else: acc
    end)
  end
end