View Source Versioned

Module Version Hex Docs Total Download License Last Updated

Note: Elixir 1.13 introduced a regression which will cause warnings for each of your versioned schema modules. It has been resolved in 1.13.2.

Versioned is a tool for enhancing Ecto.Schema modules to keep a full history of changes.

The underlying method is to create a corresponding "versions" table for each schema (with all the same columns) where each record indicates a create, update, or delete event. When a record is deleted, the versions table entry has the record in its final state, and the special is_deleted field will be set to true.

Importantly, the versions table features NO foreign key constraints. This means a couple of things. First, the auto-generated foreign key column in the versions table doesn't depend on the record in the main table, which may be deleted. Secondly, any field you define with Ecto.Migration.references/2 will only have the constraint for the main table. Here again, the referenced records can be deleted without worry of version records.

Records in the main table are mutable and operated on as normal, including deletes where the record is truly deleted.

Versioned provides helpers for migrations and schemas. The Versioned module has Versioned.insert/2, Versioned.update/2 and Versioned.delete/2 which should be used in place of your application's Repo for versioned tables. Finally, Versioned.history/3 can be used to retrieve a list of entity versions, newest first.

installation

Installation

def deps do
  [
    {:versioned, "~> 0.3.0"}
  ]
end

example

Example

defmodule MyApp.Repo.Migrations.CreateCar do
  use Versioned.Migration

  def change do
    # Creates 2 tables:
    # - "cars" with columns id, name, inserted_at and updated_at
    # - "cars_versions" with columns id, name, car_id and inserted_at
    create_versioned_table(:cars) do
      add :name, :string
    end
  end
end

defmodule MyApp.Car do
  use Versioned.Schema

  versioned_schema "cars" do
    field :name, :string
  end
end

defmodule MyApp do
  alias MyApp.Car

  def do_some_stuff do
    {:ok, %{id: car_id} = car} = Versioned.insert(%Car{name: "Toad"})

    {:ok, car} =
      car
      |> Ecto.Changeset.change()
      |> Ecto.Changeset.cast(%{name: "Magnificent"}, [:name])
      |> Versioned.update()

    {:ok, _car} = Versioned.delete(car)

    # The record is deleted.
    nil = MyApp.Repo.get(Car, car_id)

    # `Versioned.history/2` still returns all changes, newest first.
    [
      %Car.Version{car_id: ^car_id, name: "Magnificent", is_deleted: true},
      %Car.Version{car_id: ^car_id, name: "Magnificent", is_deleted: false},
      %Car.Version{car_id: ^car_id, name: "Toad", is_deleted: false}
    ] = Versioned.history(Car, car_id)
  end
end

managing-groups-of-records-together

Managing Groups of Records Together

Also of note is the library's ability to properly manage version records when inserting, updating or deleting groups of records via has_many relationships with Ecto.Changeset.cast_assoc/3. Note that creating or updating a single child record in the params for a belongs_to connection is not currently supported. In fact, there are probably other potentially useful features and pieces which have not yet been explored.

extras

Extras

migration-helper-add-column-on-a-versioned-table

Migration Helper: Add Column on a Versioned Table

Later, manage versioned tables with these convenience macros which appropriately work on the field in both tables.

defmodule MyApp.Repo.Migrations.DoCarChangeThings do
  use Versioned.Migration

  def change do
    add_versioned_column("cars", :color, :string)
    rename_versioned_column("cars", :color, to: :color_info)
    modify_versioned_column("cars", :color_info, :text, null: false)
    remove_versioned_column("cars", :color_info)

    rename_versioned_table("cars", "automobiles")
  end
end

absinthe-helper-create-a-version-object

Absinthe Helper: Create a Version Object

While versioned does not depend on Absinthe, it does provide a shortcut for creating an absinthe "version" object, wrapping one of your entities. In the following example, :car_version would have the following fields:

  • :id - primary key of the version record
  • :is_deleted - boolean indicating if the record was deleted as of this version
  • :inserted_at - UTC timestamp, indicating when the version was created
  • :car - The car as it was in this version
defmodule MyApp.Schema.Types.User do
  use Absinthe.Schema.Notation
  import Versioned.Absinthe

  object :car do
    field :id, :id
    field :name, :string
  end

  version_object :car_version, :car
end

Copyright (c) 2021 Instinct Science

This library is licensed under the MIT License.