An extension to AshPaperTrail, which is itself an extension to Ash. It augments the version resource AshPaperTrail generates, and does two independent things.

  • It lifts the action's shared context onto the version, so a version records where a change came from rather than only what changed.
  • It enqueues an Oban job when a version is created, in the data layer's transaction where the data layer opens one.

A resource asks for either or both.

Installation

This library is experimental and under active development. Its API, code, and design are subject to change.

def deps do
  [
    {:ash_paper_plane, "~> 0.1.0"}
  ]
end

The API reference is at hexdocs.pm/ash_paper_plane.

Usage

Both halves arrive through the paper_trail block's mixin option.

defmodule MyApp.Blog.Post do
  use Ash.Resource,
    domain: MyApp.Blog,
    extensions: [AshPaperTrail.Resource]

  paper_trail do
    mixin {AshPaperPlane.VersionMixin, :extend,
           [[metadata: true, worker: MyApp.VersionCreatedWorker]]}
  end
end

metadata: true adds a metadata attribute to the version and fills it. A worker enqueues a job. Neither implies the other, so a resource wanting one names one.

Six options shape the two halves, and AshPaperPlane.VersionMixin documents each with its default.

The metadata half carries what you stamp

The keys come from source_context.shared, which is the application's to fill. A key absent from the context, or carried with a nil value, is left out.

Stamping it is the caller's job, and the natural place is wherever a verified value already exists:

post
|> Ash.Changeset.for_update(:update, params)
|> Ash.Changeset.set_context(%{shared: %{origin: "web", correlation_id: id}})
|> Ash.update!()

That version records origin and correlation_id because they were set, and omits causation_id because it was not.

The Oban half rides your transaction, if you have one

The job is inserted in an after_action hook. Where the resource's data layer opens a transaction on the repo Oban is configured with, the insert joins it. A job cannot then survive an action that rolled back, and a version cannot commit while its job is lost.

Both conditions are yours to meet rather than the library's to guarantee. A resource whose data layer opens no transaction, or an Oban pointed at another repo, still enqueues its job, with nothing tying the two together.

What it leaves to you

  • Running the worker. What the job does when it runs is yours.
  • Deciding what belongs in shared. The mixin carries the keys you name.
  • The version resource itself, which is AshPaperTrail's.

Testing this library

The suite needs PostgreSQL, because Oban's assertions read its jobs table. Point it at one with POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB, or take the defaults, and run:

mix test