Current section
Files
Jump to
Current section
Files
lib/lotus/storage/query_visualization.ex
defmodule Lotus.Storage.QueryVisualization do
@moduledoc """
Represents a saved visualization configuration for a Lotus query.
The `config` field stores an opaque map that Lotus does not validate beyond
ensuring it is a map with string keys. Consumers (e.g., Lotus Web) are
responsible for validating the config according to their chosen charting library.
"""
use Ecto.Schema
import Ecto.Changeset
alias Lotus.Storage.Query
@type t :: %__MODULE__{
id: term(),
query_id: term(),
name: String.t(),
position: non_neg_integer(),
config: map(),
version: non_neg_integer(),
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@primary_key {:id, :id, autogenerate: true}
@foreign_key_type :id
json_encoder = Lotus.JSON.encoder()
@derive {json_encoder,
only: [
:id,
:query_id,
:name,
:position,
:config,
:version,
:inserted_at,
:updated_at
]}
schema "lotus_query_visualizations" do
field(:name, :string)
field(:position, :integer)
field(:config, :map)
field(:version, :integer, default: 1)
belongs_to(:query, Query, references: :id, type: :id)
timestamps(type: :utc_datetime_usec)
end
@required ~w(query_id name position config)a
@permitted @required ++ ~w(version)a
def new(attrs), do: changeset(%__MODULE__{}, attrs)
def update(viz, attrs), do: changeset(viz, attrs)
def changeset(viz, attrs) do
viz
|> cast(attrs, @permitted)
|> normalize_config()
|> validate_required(@required)
|> validate_length(:name, min: 1, max: 255)
|> validate_number(:position, greater_than_or_equal_to: 0)
|> unique_constraint(:name,
name: "lotus_query_visualizations_query_id_name_index",
message: "name must be unique within the query"
)
end
defp normalize_config(%Ecto.Changeset{} = cs) do
case get_change(cs, :config) do
nil -> cs
cfg when is_map(cfg) -> put_change(cs, :config, stringify_keys(cfg))
_ -> cs
end
end
defp stringify_keys(map) when is_map(map) do
Map.new(map, fn
{k, v} when is_atom(k) -> {Atom.to_string(k), stringify_keys(v)}
{k, v} -> {k, stringify_keys(v)}
end)
end
defp stringify_keys(list) when is_list(list), do: Enum.map(list, &stringify_keys/1)
defp stringify_keys(other), do: other
end