Autogenerated unique short IDs

HarryET
2023-01-28

HarryET:

What is the best way in Ash to generate short IDs e.g. invite codes that are unique and will keep generating till it gets a unique one? Actions?

ZachDaniel:

Yep! You can do it in a before action hook and check until you get a unique one 🙂

HarryET:

Check manually or does ash have checking built in?

ZachDaniel:

There is nothing that will repeatedly check and retry built in

HarryET:

👌🏼 So i write an action that generates then tryies to query for it, if it finds it -> generate a new one, else return

ZachDaniel:

Well, you’d just include that as a change on a create action.

HarryET:

yeah

ZachDaniel:

create :create do
  # assuming short_id is allow_nil: false, you'd use this to not require it for this action
  allow_nil_input [:short_id]

  change fn changeset, _ ->
    Ash.Changeset.before_action(changeset, fn changeset -> 
      Ash.Changeset.force_change_attribute(changeset, :short_id, generate_short_id(...))
    end)
  end
end

HarryET:

🔥 thanks yeah thats what I was plannin

HarryET:

iex(2)> Project.Resources.Invites.generate_code
[debug] QUERY OK source="invites" db=22.0ms idle=213.1ms
SELECT c0."id", c0."code", c0."sent_to", c0."max_uses", c0."created_at", c0."updated_at", c0."company_id" FROM "invites" AS c0 WHERE (c0."code"::text = $1::text) ["ExpDCJLb"]
↳ AshPostgres.DataLayer.run_query/2, at: lib/data_layer.ex:599
"ExpDCJLb"
iex(3)> 

HarryET:

  @alphabet Enum.concat([?0..?9, ?A..?Z, ?a..?z])
  @max_tries 15

  def generate_code(attempts \\ 1, len \\ 8) do
    code = for _ <- 1..len, into: "", do: <>

    with {:ok, []} <-
           Invite
           |> Ash.Query.for_read(:read)
           |> Ash.Query.filter(code == ^code)
           |> Project.Api.read() do
      code
    else
      _ ->
        if attempts > @max_tries do
          throw("failed to generate code")
        else
          generate_code(attempts + 1, len)
        end
    end
  end

HarryET:

Just putting the code here for anyone else <a:magic_sparkles:989799235274817646>

ZachDaniel:

🥳

HarryET:

An optimisation would be checking for count rather than an actual query but idk if you can do that

ZachDaniel:

Not quite yet, but I’m working on Api.aggregate(query, :count, ...) to do exactly that

Terris:

This is a tangent but you might find this interesting. https://morioh.com/p/52d5efc4cba3

HarryET:

They are a bit long for what I need xD thanks for sending though

Terris:

I know. The js ulid library I use lets you control the length and other aspects of the format. This elixir one is a bit deficient.