if Code.ensure_loaded?(Hashids) do
defmodule Ecto.Hashids do
@moduledoc """
A custom type leverages the generated sequence integer value to
encode a hashid string for the primary key, recommend to use this for
the partition key (the first defined primary key).
Define `:hashids` type to the primary key of schema, for example:
defmodule MySchema do
use EctoTablestore.Schema
schema "table_name" do
field(:id, Ecto.Hashids, primary_key: true, autogenerate: true)
end
end
In the above case, there will try to find a schema `:hashids` configuration from
the application environment:
config :ecto_tablestore,
hashids: [
{MySchema, [salt: "...", min_len: ..., alphabet: "..."]}
]
If the `:hashids` option of the application environment is not defined, there will use default
options("`[]`") to `Hashids.new/1`.
We can also set the options when define the `:hashids` type to the primary key,
for example:
field(:id, :hashids,
primary_key: true,
autogenerate: true
hashids: [salt: "...", min_len: ..., alphabet: "..."]
)
Once the above `:hashids` options defined in the `:id` field of schema, there will always use them
to encode a hashid, meanwhile the application runtime environment definition will not affect them.
Please see the options of `Hashids.new/1` for details.
"""
use Ecto.ParameterizedType
@hashids_opts [:alphabet, :min_len, :salt]
@impl true
def type(_options) do
# define type as `:binary_id` there will be processed as
# `autogenerate_id` by Ecto, and can properly return the primary key
# with the expected value into the struct after insert.
:binary_id
end
@impl true
def init(opts) do
opts
|> Keyword.get(:hashids, [])
|> Keyword.take(@hashids_opts)
|> Keyword.put(:schema, opts[:schema])
end
@impl true
def cast(value, _options) when is_bitstring(value) do
{:ok, value}
end
@impl true
def load(value, _loader, _options) do
{:ok, value}
end
@impl true
def dump(nil, _, _), do: {:ok, nil}
def dump(value, _dumper, options) when is_integer(value) and value >= 0 do
value = options |> new_hashids() |> Hashids.encode(value)
{:ok, value}
end
def dump(value, _dumper, _options) when is_bitstring(value) do
# already hashids encoded case but this dump callback still invoked by Ecto
# for example, insert and then update it with an generated hashids
{:ok, value}
end
defp new_hashids(opts) do
hashids_opts = Keyword.take(opts, @hashids_opts)
if hashids_opts == [] do
opts[:schema]
|> fetch_env_hashids_opts!()
|> opts_to_hashids()
else
opts_to_hashids(hashids_opts)
end
end
defp fetch_env_hashids_opts!(schema) do
opts =
:ecto_tablestore
|> Application.get_env(:hashids, [])
|> Keyword.get(schema, [])
if not is_list(opts),
do: raise_invalid_opts(opts)
opts
end
defp opts_to_hashids(opts) when is_list(opts) do
Hashids.new(opts)
end
defp opts_to_hashids(opts) do
raise_invalid_opts(opts)
end
defp raise_invalid_opts(opts) do
raise ArgumentError,
message: "expect a keyword as options to Hashids.new/1, but got: #{inspect(opts)}"
end
end
end