Skip to content

Commit

Permalink
Register segments schema
Browse files Browse the repository at this point in the history
  • Loading branch information
apata committed Nov 12, 2024
1 parent fc83040 commit 6c6c38a
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 0 deletions.
95 changes: 95 additions & 0 deletions lib/plausible/segment.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
defmodule Plausible.Segment do
@moduledoc """
Schema for segments. Segments are saved filter combinations.
"""
use Plausible
use Ecto.Schema
import Ecto.Changeset

@segment_types [:personal, :site]

@type t() :: %__MODULE__{}

@derive {Jason.Encoder,
only: [
:id,
:name,
:type,
:segment_data,
:owner_id,
:inserted_at,
:updated_at
]}

schema "segments" do
field :name, :string
field :type, Ecto.Enum, values: @segment_types
field :segment_data, :map

# owner ID can be null (aka segment is dangling) when the original owner is deassociated from the site
# the segment is dangling until another user edits it: the editor becomes the new owner
belongs_to :owner, Plausible.Auth.User, foreign_key: :owner_id
belongs_to :site, Plausible.Site

timestamps()
end

def changeset(segment, attrs) do
segment
|> cast(attrs, [
:name,
:segment_data,
:site_id,
:type,
:owner_id
])
|> validate_required([:name, :segment_data, :site_id, :type, :owner_id])
|> foreign_key_constraint(:site_id)
|> foreign_key_constraint(:owner_id)
|> validate_only_known_properties_present()
|> validate_segment_data_filters()
|> validate_segment_data_labels()
end

defp validate_only_known_properties_present(changeset) do
case get_field(changeset, :segment_data) do
segment_data when is_map(segment_data) ->
if Enum.any?(Map.keys(segment_data) -- ["filters", "labels"]) do
add_error(
changeset,
:segment_data,
"must not contain any other property except \"filters\" and \"labels\""
)
else
changeset
end

_ ->
changeset
end
end

defp validate_segment_data_filters(changeset) do
case get_field(changeset, :segment_data) do
%{"filters" => filters} when is_list(filters) and length(filters) > 0 ->
changeset

_ ->
add_error(
changeset,
:segment_data,
"property \"filters\" must be an array with at least one member"
)
end
end

defp validate_segment_data_labels(changeset) do
case get_field(changeset, :segment_data) do
%{"labels" => labels} when not is_map(labels) ->
add_error(changeset, :segment_data, "property \"labels\" must be map or nil")

_ ->
changeset
end
end
end
92 changes: 92 additions & 0 deletions test/plausible/segment_schema_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
defmodule Plausible.SegmentSchemaTest do
use ExUnit.Case

setup do
segment = %Plausible.Segment{
name: "any name",
type: :personal,
segment_data: %{"filters" => ["is", "visit:page", ["/blog"]]},
owner_id: 1,
site_id: 100
}

{:ok, segment: segment}
end

test "changeset has required fields" do
assert Plausible.Segment.changeset(%Plausible.Segment{}, %{}).errors == [
segment_data: {"property \"filters\" must be an array with at least one member", []},
name: {"can't be blank", [validation: :required]},
segment_data: {"can't be blank", [validation: :required]},
site_id: {"can't be blank", [validation: :required]},
type: {"can't be blank", [validation: :required]},
owner_id: {"can't be blank", [validation: :required]}
]
end

test "changeset does not allow setting owner_id to nil (setting to nil happens with database triggers)",
%{segment: valid_segment} do
assert Plausible.Segment.changeset(
valid_segment,
%{
owner_id: nil
}
).errors == [
owner_id: {"can't be blank", [validation: :required]}
]
end

test "changeset allows setting nil owner_id to a user id (to be able to recover dangling site segments)",
%{segment: valid_segment} do
assert Plausible.Segment.changeset(
%Plausible.Segment{
valid_segment
| owner_id: nil
},
%{
owner_id: 100_100
}
).valid? == true
end

test "changeset requires segment_data to be structured as expected", %{segment: valid_segment} do
assert Plausible.Segment.changeset(
valid_segment,
%{
segment_data: %{"filters" => 1, "labels" => true, "other" => []}
}
).errors == [
{:segment_data, {"property \"labels\" must be map or nil", []}},
{:segment_data,
{"property \"filters\" must be an array with at least one member", []}},
{:segment_data,
{"must not contain any other property except \"filters\" and \"labels\"", []}}
]
end

test "changeset forbids empty filters list", %{segment: valid_segment} do
assert Plausible.Segment.changeset(
valid_segment,
%{
segment_data: %{
"filters" => []
}
}
).errors == [
{:segment_data,
{"property \"filters\" must be an array with at least one member", []}}
]
end

test "changeset permits well-structured segment data", %{segment: valid_segment} do
assert Plausible.Segment.changeset(
valid_segment,
%{
segment_data: %{
"filters" => [["is", "visit:country", ["DE"]]],
"labels" => %{"DE" => "Germany"}
}
}
).valid? == true
end
end

0 comments on commit 6c6c38a

Please sign in to comment.