Intro Logo
Back to News

Uni Ecto Plugin Instant

I’ll assume “Uni” refers to a shared/umbrella application structure where you want a reusable Ecto-based plugin (e.g., multi-tenancy, auditing, soft-deletes, or encryption) that can be dropped into any context.


5. Example Workflow

# Create a payment referencing an external Stripe customer
customer_uni = UNI.parse!("uni://customer/stripe/cus_123")

changeset = Payment.changeset(%Payment{}, % customer_uni: customer_uni, amount: 49.99 )

:ok, payment = Repo.insert(changeset)

6. Writing Tests for the Plugin

test/uni_ecto_plugin/soft_delete_test.exs

defmodule UniEctoPlugin.SoftDeleteTest do
  use ExUnit.Case, async: true
  alias Ecto.Adapters.SQL.Sandbox
  # Setup a test repo (see full example in Appendix)

defmodule TestRepo do use Ecto.Repo, otp_app: :uni_ecto_plugin, adapter: Ecto.Adapters.Postgres end uni ecto plugin

defmodule User do use Ecto.Schema use UniEctoPlugin.SoftDelete

schema "users" do
  field :name, :string
  timestamps()
end

end

setup do :ok = Sandbox.checkout(TestRepo) end

test "soft_delete/2 sets deleted_at" do :ok, user = %Username: "Jane" |> TestRepo.insert() assert user.deleted_at == nil :ok, deleted_user = User.soft_delete(user, TestRepo) refute is_nil(deleted_user.deleted_at) assert User.deleted?(deleted_user) end end setup do :ok = Sandbox

test "restore/2 clears deleted_at" do user = %Username: "John", deleted_at: DateTime.utc_now() :ok, restored = User.restore(user, TestRepo) assert is_nil(restored.deleted_at) refute User.deleted?(restored) end end

7.2 Caching Resolved Entities

The plugin can cache resolved UNIs per request or globally:

use UNI.Ecto.Association, cache: :global, 60_000 # 60s TTL

Step 2: Define the Pipeline Using Uni

# lib/my_app/accounts/user_registration.ex
defmodule MyApp.Accounts.UserRegistration do
  use Uni.Step
  import Uni.Ecto

def run(attrs) do Uni.new() |> Uni.put(:original_attrs, attrs) |> add_step(:build_changeset, fn ctx -> changeset = User.registration_changeset(ctx.data.original_attrs) :ok, changeset end) |> add_step(:insert_user, insert(&1.data.build_changeset)) |> add_step(:assign_default_role, fn ctx -> user = ctx.data.insert_user updated_changeset = User.changeset(user, %role: "verified_member") :ok, updated_changeset end) |> add_step(:update_role, update(&1.data.assign_default_role)) |> add_step(:send_welcome, fn ctx -> # Imagine this calls an email service IO.inspect("Sending welcome to #ctx.data.update_role.email") :ok, %delivered: true end) |> Uni.execute() end end do: ... end

3.3 Automatic Resolution via belongs_to_uni

The plugin adds a macro belongs_to_uni:

defmodule MyApp.Payment do
  use Ecto.Schema
  use UNI.Ecto.Association, resolvers: [customer: MyApp.CustomerResolver, :resolve]

schema "payments" do field :customer_uni, UNI.Ecto.Type belongs_to_uni :customer, UNI.Ecto.Type, field: :customer_uni, resolve_with: :customer end end

The resolver:

defmodule MyApp.CustomerResolver do
  def resolve(%UNItype: "customer", origin: "stripe", local_id: id) do
    MyApp.StripeAPI.get_customer(id)
  end
  def resolve(%UNItype: "customer", origin: "internal"), do: ...
end

Now Repo.get(Payment, id) |> Repo.preload(:customer) will call the resolver transparently.