Skip to content
Merged
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
6 changes: 3 additions & 3 deletions lib/geo/geography/country.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ defmodule Geo.Geography.Country do
use Ash.Resource,
otp_app: :geo,
domain: Geo.Geography,
data_layer: AshPostgres.DataLayer,
extensions: [Geo.Resources.Attributes.Id]
data_layer: AshPostgres.DataLayer

# === Attributes ===
use Geo.Resources.Attributes.Id
use Geo.Resources.Attributes.Timestamps
use Geo.Resources.Attributes.Name, allow_nil?: false, unique?: true
use Geo.Resources.Attributes.Slug, allow_nil?: false, unique?: true
use Geo.Resources.Attributes.Timestamps

attributes do
attribute :iso_code, :ci_string do
Expand Down
30 changes: 5 additions & 25 deletions lib/geo/resources/attributes/id.ex
Original file line number Diff line number Diff line change
@@ -1,32 +1,12 @@
defmodule Geo.Resources.Attributes.Id do
@moduledoc """
An Ash extension that adds a UUID v7 primary key attribute to a resource.
"""

use Spark.Dsl.Extension,
transformers: [
__MODULE__.Transformer
]

defmodule Transformer do
@moduledoc false
use Spark.Dsl.Transformer

def before?(Ash.Resource.Transformers.BelongsToAttribute), do: true
def before?(_), do: false

def transform(dsl_state) do
attribute = %Ash.Resource.Attribute{
name: :id,
type: :uuid_v7,
allow_nil?: false,
writable?: false,
public?: true,
primary_key?: true,
default: &Ash.UUIDv7.generate/0
}

{:ok, Spark.Dsl.Transformer.add_entity(dsl_state, [:attributes], attribute)}
defmacro __using__(_opts) do
quote do
attributes do
uuid_v7_primary_key :id
end
end
end
end
61 changes: 59 additions & 2 deletions lib/geo/resources/changes/slugify_name.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,72 @@ defmodule Geo.Resources.Changes.SlugifyName do

# If we have a name but no slug, generate one
not is_nil(name) ->
generated_slug = slugify(name)
Ash.Changeset.force_change_attribute(changeset, :slug, generated_slug)
base_slug = slugify(name)
unique_slug = ensure_unique_slug(changeset, base_slug)
Ash.Changeset.force_change_attribute(changeset, :slug, unique_slug)

# Otherwise, leave as is
true ->
changeset
end
end

defp ensure_unique_slug(changeset, base_slug) do
# Try base slug first
if !slug_exists?(changeset, base_slug) do
base_slug
else
find_unique_slug_with_number(changeset, base_slug)
end
end

defp find_unique_slug_with_number(changeset, base_slug) do
# Range 1: 1-9
case find_in_range(changeset, base_slug, 1..9) do
{:found, number} -> "#{base_slug}-#{number}"
:not_found ->
# Range 2: 1-99
case find_in_range(changeset, base_slug, 1..99) do
{:found, number} -> "#{base_slug}-#{number}"
:not_found ->
# Range 3: 1-9999
case find_in_range(changeset, base_slug, 1..9999) do
{:found, number} -> "#{base_slug}-#{number}"
:not_found -> raise "Could not find unique slug after trying up to 9999"
end
end
end
end

defp find_in_range(changeset, base_slug, range) do
Enum.find_value(range, :not_found, fn number ->
slug_candidate = "#{base_slug}-#{number}"
if !slug_exists?(changeset, slug_candidate) do
{:found, number}
end
end)
end

defp slug_exists?(changeset, slug) do
resource = changeset.resource
query = Ash.Query.filter(resource, slug: slug)

# Exclude the current record if it's an update
query =
case changeset.data do
%{id: id} when not is_nil(id) ->
Ash.Query.filter(query, id != ^id)
_ ->
query
end

case Ash.read(query) do
{:ok, []} -> false
{:ok, _} -> true
{:error, _} -> false
end
end

defp slugify(text) when is_binary(text) do
text
|> String.downcase()
Expand Down
Loading