fireproofsocks

fireproofsocks

I’m working on a table to store shipping addresses. I would like to add a fingerprint of each address so that I can more easily tell whether or not a given address has already been added. For simplicity, let’s assume that I’ve worked out a nice fingerprinting algorithm to normalize my addresses and identify them via a single string… let’s say this function is named simply fingerprint(address).

My question is about Ecto and changesets: how and where can I add this field to the database record when I save it? I’m unclear on how the cast and other functions are working inside the changeset function.

If I try to save a new address and its fingerprint matches an existing address, the save operation should fail.
If I try to edit an existing address and change it so it matches an existing address, the update operation should fail.

Any help on this would be appreciated… I’m having a hard time wrapping my head around where to add this and how to think about it in an idiomatic way. Thanks!

Showing Posts 1 to 10

kokolegorille

kokolegorille

Probably something like this…

  @doc false
  def changeset(whatever, attrs) do
    whatever
    |> changeset(attrs)
    |> cast(attrs, @fields)
    |> validate ...
    |> fingerprint()
    |> unique_constraint(:fingerprint, message: "Fingerprint already in use")
  end

  defp fingerprint(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{address: address}} ->
        put_change(changeset, :fingerprint, create_fingerprint(address))
      _ ->
        changeset
    end
  end

  defp create_fingerprint(address) do
    # Implement your fingerprint here
  end

It is not tested, and fields might not correspond to your structure, but You can get the idea… the 2 last lines from changeset are important.

axelson

axelson

Scenic Core Team

I’d also recommend you store the fingerprint in the database and add a unique constraint and a non-null constraint on the column.

fireproofsocks

fireproofsocks OP

defp fingerprint(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{address: address}} ->
        put_change(changeset, :fingerprint, create_fingerprint(address))
      _ ->
        changeset
    end
  end

Whoa. Mind blown. First off, thank you – this is way further than I would have gotten even after pouring over the Ecto docs. This code does execute, but the “fingerprint” value does not get set. I can test the create_fingerprint() function and verify that it returns a string value when given a map representing an address. I confess that I do not understand much of anything (including the syntax) of the defp fingerprint() function in your example. Can anyone walk me through that code? Or maybe someone can spot why that value isn’t persisting?

As always, many thanks.

kokolegorille

kokolegorille

You might show your schema, and migration file. Be sure :fingerprint field exists, as string…

You might also insert some IO.inspect in your pipeline to show the changeset, like so

  def changeset(whatever, attrs) do
    whatever
    |> changeset(attrs)
    |> cast(attrs, @fields)
    |> validate ...
    |> IO.inspect()
    |> fingerprint()
    |> IO.inspect()
    |> unique_constraint(:fingerprint, message: "Fingerprint already in use")
  end

The function fingerprint take a changeset, and if it is valid, it should persists the change.

If it is clearer, You could have used pattern matching, like so.

defp fingerprint(%Ecto.Changeset{valid?: true, changes: %{address: address}} = changeset) do
  put_change(changeset, :fingerprint, create_fingerprint(address))
end
defp fingerprint(changeset), do: changeset

BTW Try to change the name of the function to generate_fingerprint(), just to be sure it does not conflict with the field name.

fireproofsocks

fireproofsocks OP

defmodule Address.AddressCtx.Address do
  use Ecto.Schema
  import Ecto.Changeset

  schema "addresses" do
    field :street1, :string, size: 250, default: ""
    field :street2, :string, size: 250, default: ""
    field :city, :string, size: 100, default: ""
    field :state, :string, size: 2, default: ""
    field :zip, :string, size: 32, default: ""
    field :country, :string, size: 3, default: "US"
    field :fingerprint, :string, size: 32, default: ""
    timestamps()
  end

  @doc false
  def changeset(address, attrs) do
    address
    |> cast(attrs, [:street1, :street2, :city, :state, :zip, :country, :fingerprint])
    |> validate_required([:street1, :city, :state, :zip, :country])
    |> append_fingerprint()
    |> unique_constraint(:fingerprint, message: "Fingerprint already in use")
  end


  defp append_fingerprint(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{address: address}} ->
        IO.puts(address)
        put_change(changeset, :fingerprint, get_fingerprint(address))
      _ ->
        changeset
    end
  end


  @doc """
   Simple md5 fingerprint
  """
  def get_fingerprint(address) do
    address.street1 <> address.street2 <> address.city <> address.state <> address.zip <> address.country
    |> String.upcase
    |> :erlang.md5
    |> Base.encode16(case: :lower)
  end

end

Here’s my schema file – the get_fingerprint() function seems to work when I test it in iex, so I’m thinking I’ve missed something in the cast() or other functions inside changeset since I’m not fully understanding the pieces of that.

Are there times when you would want to define a field in the database but not list it in the fields passed to cast()?

kokolegorille

kokolegorille

Do not cast :fingerprint… it will be calculated :slight_smile:

fireproofsocks

fireproofsocks OP

I tried omitting that, but the result is the same. I guess I don’t understand what cast() is really doing (or the @fields macro for that matter)… even if I include a value for for the fingerprint, my expectation is that it would be overwritten anyhow.

kokolegorille

kokolegorille

The @fields is just equal to a list of fields, as your [:street1, :street2, :city, :state, :zip, :country, :fingerprint]

cast() transforms data from the outside world, to the inner struct.

Did You get some logs from IO.inspect()? I am curious to see how the changeset looks.

kokolegorille

kokolegorille

This should not work… as it should be a field

You should use

%Ecto.Changeset{valid?: true, changes: %{street1: street1, street2: street2...}}

# or

%Ecto.Changeset{valid?: true, changes: address}

In the example I gave, whatever is the shemas, and address is just a field of %Whatever{}

You might find ecto changeset structure here. From docs…

changes - The changes from parameters that were approved in casting

jordiee

jordiee

You can also use get_field(changeset, key, default \ nil) to check against changes and then the changeset data for your address prop. You issue may be be that address is not in the changes obj.

Where Next? Top

Trending in Questions Top

water
I’m aware of 2 such projects, are there others? git: GitHub - team-alembic/realworld: A fullstack Phoenix LiveView application with back...
New
katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
bradley
I really like the adapter patterns that ecto, nebulex, waffle, etc. use and would love find something similar for a key management servic...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
Alvinkariuki
How Can I Optimise Compile Time Dependencies I have been building an elixir application for about 2 years now. Many modules and files ha...
New
leoschweizer
Hi, we run different LLM models across a cluster, with each node loading a configurable subset. Model-specific queues let nodes consume ...
New

Other Trending Topics Top

KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews