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.