Coder Social home page Coder Social logo

izelnakri / paper_trail Goto Github PK

View Code? Open in Web Editor NEW
540.0 7.0 88.0 1.58 MB

Track and record all the changes in your database with Ecto. Revert back to anytime in history.

Home Page: https://hex.pm/packages/paper_trail

License: MIT License

Elixir 99.13% Dockerfile 0.29% Shell 0.59%
database versioning elixir ecto papertrail phoenix history audit-log

paper_trail's Introduction

Hex Version Hex docs Total Download License Last Updated

Paper Trail

Track and record all the changes in your database. Revert back to anytime in history.

How does it work?

PaperTrail lets you record every change in your database in a separate database table called versions. Library generates a new version record with associated data every time you run PaperTrail.insert/2, PaperTrail.update/2 or PaperTrail.delete/2 functions. Simply these functions wrap your Repo insert, update or destroy actions in a database transaction, so if your database action fails you won't get a new version.

PaperTrail is assailed with hundreds of test assertions for each release. Data integrity is an important aim of this project, please refer to the strict_mode if you want to ensure data correctness and integrity of your versions. For simpler use cases the default mode of PaperTrail should suffice.

Example

changeset = Post.changeset(%Post{}, %{
  title: "Word on the street is Elixir got its own database versioning library",
  content: "You should try it now!"
})

PaperTrail.insert(changeset)
# => on success:
# {:ok,
#  %{model: %Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
#     title: "Word on the street is Elixir got its own database versioning library",
#     content: "You should try it now!", id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#     updated_at: ~N[2016-09-15 21:42:38]},
#    version: %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#     event: "insert", id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#     item_changes: %{title: "Word on the street is Elixir got its own database versioning library",
#       content: "You should try it now!", id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#       updated_at: ~N[2016-09-15 21:42:38]},
#     item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}}}

# => on error(it matches Repo.insert/2):
# {:error, Ecto.Changeset<action: :insert,
#  changes: %{title: "Word on the street is Elixir got its own database versioning library", content: "You should try it now!"},
#  errors: [content: {"is too short", []}], data: #Post<>,
#  valid?: false>, %{}}

post = Repo.get!(Post, 1)
edit_changeset = Post.changeset(post, %{
  title: "Elixir matures fast",
  content: "Future is already here, Elixir is the next step!"
})

PaperTrail.update(edit_changeset)
# => on success:
# {:ok,
#  %{model: %Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
#     title: "Elixir matures fast", content: "Future is already here, Elixir is the next step!",
#     id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#     updated_at: ~N[2016-09-15 22:00:59]},
#    version: %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#     event: "update", id: 2, inserted_at: ~N[2016-09-15 22:00:59],
#     item_changes: %{title: "Elixir matures fast", content: "Future is already here, Elixir is the next step!"},
#     item_id: 1, item_type: "Post", originator_id: nil, originator: nil
#     meta: nil}}}

# => on error(it matches Repo.update/2):
# {:error, Ecto.Changeset<action: :update,
#  changes: %{title: "Elixir matures fast", content: "Future is already here, Elixir is the next step!"},
#  errors: [title: {"is too short", []}], data: #Post<>,
#  valid?: false>, %{}}

PaperTrail.get_version(post)
#  %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#   event: "update", id: 2, inserted_at: ~N[2016-09-15 22:00:59],
#   item_changes: %{title: "Elixir matures fast", content: "Future is already here, Elixir is the next step!"},
#   item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}}}

updated_post = Repo.get!(Post, 1)

PaperTrail.delete(updated_post)
# => on success:
# {:ok,
#  %{model: %Post{__meta__: #Ecto.Schema.Metadata<:deleted, "posts">,
#     title: "Elixir matures fast", content: "Future is already here, Elixir is the next step!",
#     id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#     updated_at: ~N[2016-09-15 22:00:59]},
#    version: %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#     event: "delete", id: 3, inserted_at: ~N[2016-09-15 22:22:12],
#     item_changes: %{title: "Elixir matures fast", content: "Future is already here, Elixir is the next step!",
#       id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#       updated_at: ~N[2016-09-15 22:00:59]},
#     item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}}}

Repo.aggregate(Post, :count, :id) # => 0
PaperTrail.Version.count() # => 3
# same as Repo.aggregate(PaperTrail.Version, :count, :id)

PaperTrail.Version.last() # returns the last version in the db by inserted_at
#  %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#   event: "delete", id: 3, inserted_at: ~N[2016-09-15 22:22:12],
#   item_changes: %{"title" => "Elixir matures fast", content: "Future is already here, Elixir is the next step!", "id" => 1,
#     "inserted_at" => "2016-09-15T21:42:38",
#     "updated_at" => "2016-09-15T22:00:59"},
#   item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}

PaperTrail is inspired by the ruby gem paper_trail. However, unlike the paper_trail gem this library actually results in less data duplication, faster and more explicit programming model to version your record changes.

The library source code is minimal and well tested. It is suggested to read the source code.

Installation

  1. Add paper_trail to your list of dependencies in mix.exs:

    def deps do
      [{:paper_trail, "~> 0.14.3"}]
    end
  2. Configure paper_trail to use your application repo in config/config.exs:

    config :paper_trail, repo: YourApplicationName.Repo
    # if you don't specify this PaperTrail will assume your repo name is Repo
  3. Install and compile your dependency:

    mix deps.get && mix compile

  4. Run this command to generate the migration:

    mix papertrail.install

    You might want to edit the types for :item_id or :originator_id if you're using UUID or other types for your primary keys before you execute mix ecto.migrate.

  5. Run the migration:

    mix ecto.migrate

Your application is now ready to collect some history!

Does this work with phoenix?

YES! Make sure you do the steps above.

%PaperTrail.Version{} fields:

Column Name Type Description Entry Method
event String either "insert", "update" or "delete" Library generates
item_type String model name of the reference record Library generates
item_id configurable (Integer by default) model id of the reference record Library generates
item_changes Map all the changes in this version as a map Library generates
originator_id configurable (Integer by default) foreign key reference to the creator/owner of this change Optionally set
origin String short reference to origin(eg. worker:activity-checker, migration, admin:33) Optionally set
meta Map any extra optional meta information about the version(eg. %{slug: "ausername", important: true}) Optionally set
inserted_at Date inserted_at timestamp Ecto generates

Configuring the types

If you are using UUID or another type for your primary keys, you can configure the PaperTrail.Version schema to use it.

Example Config
config :paper_trail, item_type: Ecto.UUID,
                     originator_type: Ecto.UUID,
                     originator_relationship_options: [references: :uuid]
Example User
defmodule Acme.User do
  use Ecto.Schema

  @primary_key {:uuid, :binary_id, autogenerate: true}
  schema "users" do
    field :email, :string

    timestamps()
  end

Remember to edit the types accordingly in the generated migration.

Version origin references:

PaperTrail records have a string field called origin. PaperTrail.insert/2, PaperTrail.update/2, PaperTrail.delete/2 functions accept a second argument to describe the origin of this version:

PaperTrail.update(changeset, origin: "migration")
# or:
PaperTrail.update(changeset, origin: "user:1234")
# or:
PaperTrail.delete(changeset, origin: "worker:delete_inactive_users")
# or:
PaperTrail.insert(new_user_changeset, origin: "password_registration")
# or:
PaperTrail.insert(new_user_changeset, origin: "facebook_registration")

Version originator relationships

You can specify setter/originator relationship to paper_trail versions with originator assignment. This feature is only possible by specifying :originator keyword list for your application configuration:

# In your config/config.exs
config :paper_trail, originator: [name: :user, model: YourApp.User]
# For most applications originator should be the user since models can be updated/created/deleted by several users.

Note: You will need to recompile your deps after you have added the config for originator.

Then originator name could be used for querying and preloading. Originator setting must be done via :originator or originator name that is defined in the paper_trail configuration:

user = create_user()
# all these set originator_id's for the version records
PaperTrail.insert(changeset, originator: user)
{:ok, result} = PaperTrail.update(edit_changeset, originator: user)
# or you can use :user in the params instead of :originator if this is your config:
# config :paper_trail, originator: [name: :user, model: YourApplication.User]
{:ok, result} = PaperTrail.update(edit_changeset, user: user)
result[:version] |> Repo.preload(:user) |> Map.get(:user) # we can access the user who made the change from the version thanks to originator relationships!
PaperTrail.delete(edit_changeset, user: user)

Also make sure you have the foreign-key constraint in the database and in your version migration file.

Storing version meta data

You might want to add some meta data that doesn't belong to originator and origin fields. Such data could be stored in one object named meta in paper_trail versions. Meta field could be passed as the second optional parameter to PaperTrail.insert/2, PaperTrail.update/2, PaperTrail.delete/2 functions:

company = Company.changeset(%Company{}, %{name: "Acme Inc."})
  |> PaperTrail.insert(meta: %{slug: "acme-llc"})

# You can also combine this with an origin:
edited_company = Company.changeset(company, %{name: "Acme LLC"})
  |> PaperTrail.update(origin: "documentation", meta: %{slug: "acme-llc"})

# Or even with an originator:
user = create_user()
deleted_company = Company.changeset(edited_company, %{})
  |> PaperTrail.delete(origin: "worker:github", originator: user, meta: %{slug: "acme-llc", important: true})

Strict mode

This is a feature more suitable for larger applications. Models can keep their version references via foreign key constraints. Therefore it would be impossible to delete the first and current version of a model if the model exists in the database, it also makes querying easier and the whole design more relational database/SQL friendly. In order to enable strict mode:

# In your config/config.exs
config :paper_trail, strict_mode: true

Strict mode expects tracked models to have foreign-key reference to their first_version and current_version. These columns must be named first_version_id, and current_version_id in their respective model tables. A tracked model example with a migration file:

# In the migration file: priv/repo/migrations/create_company.exs
defmodule Repo.Migrations.CreateCompany do
  def change do
    create table(:companies) do
      add :name,       :string, null: false
      add :founded_in, :date

      # null constraints are highly suggested:
      add :first_version_id, references(:versions), null: false
      add :current_version_id, references(:versions), null: false

      timestamps()
    end

    create unique_index(:companies, [:first_version_id])
    create unique_index(:companies, [:current_version_id])
  end
end

# In the model definition:
defmodule Company do
  use Ecto.Schema

  import Ecto.Changeset

  schema "companies" do
    field :name, :string
    field :founded_in, :date

    belongs_to :first_version, PaperTrail.Version
    belongs_to :current_version, PaperTrail.Version, on_replace: :update # on_replace: is important!

    timestamps()
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :founded_in])
  end
end

When you run PaperTrail.insert/2 transaction, first_version_id and current_version_id automagically gets assigned for the model. Example:

company = Company.changeset(%Company{}, %{name: "Acme LLC"}) |> PaperTrail.insert
# {:ok,
#  %{model: %Company{__meta__: #Ecto.Schema.Metadata<:loaded, "companies">,
#     name: "Acme LLC", founded_in: nil, id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#     updated_at: ~N[2016-09-15 21:42:38], first_version_id: 1, current_version_id: 1},
#    version: %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#      event: "insert", id: 1, inserted_at: ~N[2016-09-15 22:22:12],
#      item_changes: %{name: "Acme LLC", founded_in: nil, id: 1, inserted_at: ~N[2016-09-15 21:42:38]},
#      originator_id: nil, origin: "unknown", meta: nil}}}

When you PaperTrail.update/2 a model, current_version_id gets updated during the transaction:

edited_company = Company.changeset(company, %{name: "Acme Inc."}) |> PaperTrail.update(origin: "documentation")
# {:ok,
#  %{model: %Company{__meta__: #Ecto.Schema.Metadata<:loaded, "companies">,
#     name: "Acme Inc.", founded_in: nil, id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#     updated_at: ~N[2016-09-15 23:22:12], first_version_id: 1, current_version_id: 2},
#    version: %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#      event: "update", id: 2, inserted_at: ~N[2016-09-15 23:22:12],
#      item_changes: %{name: "Acme Inc."}, originator_id: nil, origin: "documentation", meta: nil}}}

Additionally, you can put a null constraint on origin column, you should always put an origin reference to describe who makes the change. This is important for big applications because a model can change from many sources.

Bang(!) functions:

PaperTrail also supports PaperTrail.insert!, PaperTrail.update!, PaperTrail.delete!. Naming of these functions intentionally match Repo.insert!, Repo.update!, Repo.delete! functions. If PaperTrail is on strict_mode these bang functions will update the version references of the model just like the normal PaperTrail operations.

Bang functions assume the operation will always be successful, otherwise functions will raise Ecto.InvalidChangesetError just like Repo.insert!, Repo.update! and Repo.delete!:

changeset = Post.changeset(%Post{}, %{
  title: "Word on the street is Elixir got its own database versioning library",
  content: "You should try it now!"
})

inserted_post = PaperTrail.insert!(changeset)
# => on success:
# %Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
#   title: "Word on the street is Elixir got its own database versioning library",
#   content: "You should try it now!", id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#   updated_at: ~N[2016-09-15 21:42:38]
# }
#
# => on error raises: Ecto.InvalidChangesetError !!

inserted_post_version = PaperTrail.get_version(inserted_post)
# %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#   event: "insert", id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#   item_changes: %{title: "Word on the street is Elixir got its own database versioning library",
#     content: "You should try it now!", id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#     updated_at: ~N[2016-09-15 21:42:38]},
#   item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}

edit_changeset = Post.changeset(inserted_post, %{
  title: "Elixir matures fast",
  content: "Future is already here, Elixir is the next step!"
})

updated_post = PaperTrail.update!(edit_changeset)
# => on success:
# %Post{__meta__: #Ecto.Schema.Metadata<:loaded, "posts">,
#   title: "Elixir matures fast", content: "Future is already here, you deserve to be awesome!",
#   id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#   updated_at: ~N[2016-09-15 22:00:59]}
#
# => on error raises: Ecto.InvalidChangesetError !!

updated_post_version = PaperTrail.get_version(updated_post)
# %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#   event: "update", id: 2, inserted_at: ~N[2016-09-15 22:00:59],
#   item_changes: %{title: "Elixir matures fast", content: "Future is already here, Elixir is the next step!"},
#   item_id: 1, item_type: "Post", originator_id: nil, originator: nil
#   meta: nil}

PaperTrail.delete!(updated_post)
# => on success:
# %Post{__meta__: #Ecto.Schema.Metadata<:deleted, "posts">,
#   title: "Elixir matures fast", content: "Future is already here, Elixir is the next step!",
#   id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#   updated_at: ~N[2016-09-15 22:00:59]}
#
# => on error raises: Ecto.InvalidChangesetError !!

PaperTrail.get_version(updated_post)
# %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#   event: "delete", id: 3, inserted_at: ~N[2016-09-15 22:22:12],
#   item_changes: %{title: "Elixir matures fast", content: "Future is already here, Elixir is the next step!",
#   id: 1, inserted_at: ~N[2016-09-15 21:42:38],
#   updated_at: ~N[2016-09-15 22:00:59]},
#   item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}

Repo.aggregate(Post, :count, :id) # => 0
PaperTrail.Version.count() # => 3
# same as Repo.aggregate(PaperTrail.Version, :count, :id)

PaperTrail.Version.last() # returns the last version in the db by inserted_at
#  %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
#   event: "delete", id: 3, inserted_at: ~N[2016-09-15 22:22:12],
#   item_changes: %{"title" => "Elixir matures fast", content: "Future is already here, Elixir is the next step!", "id" => 1,
#     "inserted_at" => "2016-09-15T21:42:38",
#     "updated_at" => "2016-09-15T22:00:59"},
#   item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}

Working with multi tenancy

Sometimes you have to deal with applications where you need multi tenancy capabilities, and you want to keep tracking of the versions of your data on different schemas (PostgreSQL) or databases (MySQL).

You can use the Ecto.Query prefix in order to switch between different schemas/databases for your own data, so you can specify in your changeset where to store your record. Example:

tenant = "tenant_id"
changeset = User.changeset(%User{}, %{first_name: "Izel", last_name: "Nakri"})

changeset
|> Ecto.Queryable.to_query()
|> Map.put(:prefix, tenant)
|> Repo.insert()

PaperTrail also allows you to store the Version entries generated by your activity in different schemas/databases by using the value of the element :prefix on the options of the functions. Example:

tenant = "tenant_id"

changeset =
  User.changeset(%User{}, %{first_name: "Izel", last_name: "Nakri"})
  |> Ecto.Queryable.to_query()
  |> Map.put(:prefix, tenant)

PaperTrail.insert(changeset, [prefix: tenant])

By doing this, you're storing the new User entry into the schema/database specified by the :prefix value (tenant_id).

Note that the User's changeset it's sent with the :prefix, so PaperTrail will take care of the storage of the generated Version entry in the desired schema/database. Make sure to add this prefix to your changeset before the execution of the PaperTrail function if you want to do versioning on a separate schema.

PaperTrail can also get versions of records or models from different schemas/databases as well by using the :prefix option. Example:

tenant = "tenant_id"
id = 1

PaperTrail.get_versions(User, id, [prefix: tenant])

Version timestamps

PaperTrail can be configured to use utc_datetime or utc_datetime_usec for Version timestamps.

# In your config/config.exs
config :paper_trail, timestamps_type: :utc_datetime

Note: You will need to recompile your deps after you have added the config for timestamps.

Suggestions

  • PaperTrail.Version(s) order matter,
  • Don't delete your paper_trail versions, instead you can merge them
  • If you have a question or a problem, do not hesitate to create an issue or submit a pull request

Contributing

set -a
source .env
mix test --trace

Credits

Many thanks to:

Additional thanks to:

License

This source code is licensed under the MIT license. Copyright (c) 2016-present Izel Nakri.

paper_trail's People

Contributors

asiniy avatar dependabot-preview[bot] avatar dependabot[bot] avatar dimitridewit avatar discostarslayer avatar drapergeek avatar dreamingechoes avatar eahanson avatar floriangerhardt avatar fv316 avatar gabrielpra1 avatar ggpasqualino avatar glennr avatar hdtafur avatar izelnakri avatar joshuataylor avatar kianmeng avatar maennchen avatar marioimr avatar mindreframer avatar mitchellhenke avatar mstratman avatar narrowtux avatar nickolaich avatar ottobar avatar rschef avatar rustamtolipov avatar superhawk610 avatar szaboat avatar versilov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

paper_trail's Issues

Question on embeds_many

Hi,
I am not sure, if I am using things wrong, or if this is the wanted behaviour.

I have a schema which embeds_many(:entities, Entity, on_replace: :delete).
In my update changeset I do cast_embed(:entities, with: &Entity.update_changeset/2).

This results in a version like this:

%PaperTrail.Version{
  __meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
  event: "update",
  id: 165,
  inserted_at: ~U[2021-01-20 13:03:08Z],
  item_changes: %{
    "entities" => [
      %{
        "id" => nil,
        "language" => nil,
        "path" => nil,
        "value" => "UPDATED_VALUE"
      },
      %{"id" => nil, "language" => nil, "path" => nil, "value" => nil},
      %{"id" => nil, "language" => nil, "path" => nil, "value" => nil},
      %{"id" => nil, "language" => nil, "path" => nil, "value" => nil}
    ]
  },
  item_id: "a9c5fb77-3af7-406d-b9dd-1e5fe7266d16",
  item_type: "Document",
  meta: nil,
  origin: nil,
  originator_id: nil
}

I am wondering, why unchanged entities are listed in item_changes.
And somehow it would be helpful to also track the id of the changed entity.
Is there a way to achieve this somehow?

Any feedback is very welcome.

What information is persisted?

Hey!

This looks like a valuable library, I really appreciate the commitment to testing and robustness. What information is stored for each version? A snapshot of the changes? or a snapshot of the whole row?

error when trying update post with nested fields

When i edit and save post with nested fields without change base post fields papertrail return error

** (Poison.EncodeError) unable to encode value: {"does not exist", []}
    (poison) lib/poison/encoder.ex:383: Poison.Encoder.Any.encode/2
    (poison) lib/poison/encoder.ex:227: anonymous fn/4 in Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:259: anonymous fn/3 in Poison.Encoder.List.encode/3
    (poison) lib/poison/encoder.ex:260: Poison.Encoder.List.encode/3
    (poison) lib/poison/encoder.ex:227: anonymous fn/4 in Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:259: anonymous fn/3 in Poison.Encoder.List.encode/3
    (poison) lib/poison/encoder.ex:260: Poison.Encoder.List.encode/3
    (poison) lib/poison/encoder.ex:227: anonymous fn/4 in Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map.encode/3
    (poison) lib/poison.ex:41: Poison.encode!/2
    (ecto) /home/bohdan/work/three_taps/deps/postgrex/lib/postgrex/type_module.ex:715: Ecto.Adapters.Postgres.TypeModule.encode_params/3
    (postgrex) lib/postgrex/query.ex:45: DBConnection.Query.Postgrex.Query.encode/3
    (db_connection) lib/db_connection.ex:1079: DBConnection.describe_run/5
    (db_connection) lib/db_connection.ex:1150: anonymous fn/4 in DBConnection.run_meter/5
    (db_connection) lib/db_connection.ex:592: DBConnection.prepare_execute/4

When i update record without papertrail it works well.

my changeset

#Ecto.Changeset<action: nil,
 changes: %{images: [#Ecto.Changeset<action: :insert,
     changes: %{delete: false,
       url: %{file_name: "Screenshot_2018-03-21-19-50-05-445_ua.slando.png",
         updated_at: #Ecto.DateTime<2018-06-15 11:39:45>},
       uuid: "c8e65420-7090-11e8-982b-c85b76d3a09f"}, errors: [],
     data: #Olist.Images.Image<>, valid?: true>]}, errors: [],
 data: #Olist.Postings.Posting<>, valid?: true>

to save use

posting
    |> Posting.changeset(attrs)
    |> PaperTrail.update(user: user)

Support for UUID primary keys?

I'm hoping to use PaperTrail in a Phoenix project where all primary keys on database tables are UUIDs instead of integers.

Adapting the add_versions migration generated by PaperTrail to use UUIDs is fairly straightforward:

  def change do
    create table(:versions, primary_key: false) do
      add :id, :binary_id, primary_key: true

      add :event,        :string, null: false, size: 10
      add :item_type,    :string, null: false
      add :item_id,      :binary_id
      add :item_changes, :map, null: false
      add :originator_id, references(:users, type: :binary_id)
      add :origin,       :string, size: 50
      add :meta,         :map

      add :inserted_at,  :utc_datetime, null: false
    end

However, as you might expect, actually attempting to insert/update a version fails because the item_id and originator_id in the version schema are still set to :integer:

iex(4)> PaperTrail.insert(changeset)
** (Ecto.ChangeError) value `"070e8aba-85bd-4375-993d-0bdcc1ceb76f"` for `PaperTrail.Version.item_id` in `insert` does not match type :integer
             (ecto) lib/ecto/repo/schema.ex:699: Ecto.Repo.Schema.dump_field!/6
             (ecto) lib/ecto/repo/schema.ex:708: anonymous fn/6 in Ecto.Repo.Schema.dump_fields!/5
           (stdlib) lists.erl:1263: :lists.foldl/3
             (ecto) lib/ecto/repo/schema.ex:706: Ecto.Repo.Schema.dump_fields!/5
             (ecto) lib/ecto/repo/schema.ex:655: Ecto.Repo.Schema.dump_changes!/6
             (ecto) lib/ecto/repo/schema.ex:200: anonymous fn/13 in Ecto.Repo.Schema.do_insert/4
             (ecto) lib/ecto/multi.ex:406: Ecto.Multi.apply_operation/5
           (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
             (ecto) lib/ecto/multi.ex:396: anonymous fn/5 in Ecto.Multi.apply_operations/5
             (ecto) lib/ecto/adapters/sql.ex:615: anonymous fn/3 in Ecto.Adapters.SQL.do_transaction/3
    (db_connection) lib/db_connection.ex:1274: DBConnection.transaction_run/4
    (db_connection) lib/db_connection.ex:1198: DBConnection.run_begin/3
    (db_connection) lib/db_connection.ex:789: DBConnection.transaction/3
             (ecto) lib/ecto/repo/queryable.ex:21: Ecto.Repo.Queryable.transaction/4
                    lib/paper_trail.ex:91: PaperTrail.insert/2
iex(4)>

I would hope to be able to set the primary key type as a top-level configuration value to :paper_trail, e.g. something like this in config.exs:

config :paper_trail,
  repo: MyProject.Repo,
  key_type: :binary_id

I am new to Elixir, and less-than-confident in my ability to write a solid patch for this, but I'm happy to take a stab at it if you don't have the bandwidth or interest in working on it yourself.

Unable to change item_id type

I've confirmed that my config setting is correct:

iex([email protected])6> Application.get_env(:paper_trail, :item_type, :integer)
Ecto.UUID

I've updated my migration and confirmed that the field is uuid in the DB, yet when I try and send one through, I get this error:

16:15:07.811 [error] Task #PID<0.1710.0> started from #PID<0.1013.0> terminating
** (Ecto.ChangeError) value `"c7fcaf33-04b1-4e24-b0da-8c38a9cdf3e8"` for `PaperTrail.Version.item_id` in `insert` does not match type :integer
    (ecto) lib/ecto/repo/schema.ex:783: Ecto.Repo.Schema.dump_field!/6
    (ecto) lib/ecto/repo/schema.ex:792: anonymous fn/6 in Ecto.Repo.Schema.dump_fields!/5
    (stdlib) lists.erl:1263: :lists.foldl/3
    (ecto) lib/ecto/repo/schema.ex:790: Ecto.Repo.Schema.dump_fields!/5
    (ecto) lib/ecto/repo/schema.ex:739: Ecto.Repo.Schema.dump_changes!/6
    (ecto) lib/ecto/repo/schema.ex:206: anonymous fn/14 in Ecto.Repo.Schema.do_insert/4
    (ecto) lib/ecto/multi.ex:422: Ecto.Multi.apply_operation/5
    (elixir) lib/enum.ex:1811: Enum."-reduce/3-lists^foldl/2-0-"/3
    (ecto) lib/ecto/multi.ex:412: anonymous fn/5 in Ecto.Multi.apply_operations/5
    (ecto) lib/ecto/adapters/sql.ex:576: anonymous fn/3 in Ecto.Adapters.SQL.do_transaction/3
    (db_connection) lib/db_connection.ex:1275: DBConnection.transaction_run/4
    (db_connection) lib/db_connection.ex:1199: DBConnection.run_begin/3
    (db_connection) lib/db_connection.ex:790: DBConnection.transaction/3
    (ecto) lib/ecto/repo/queryable.ex:23: Ecto.Repo.Queryable.transaction/4
    (paper_trail) lib/paper_trail.ex:152: PaperTrail.update/2

Am I missing something? I've restarted my servers, even re-migrated everything from scratch and still getting this.. Thanks!

FYI - I'm using Ecto 2.2.1 + Paper Trail 0.7.7 and this is being used inside an umbrella app. My guess is the latter might have something to do with it?

Add Formatter to CI Workflow

Add mix format --check-formatted to CI to make sure future PRs comply with the Elixir Formatter.

I'll provide a PR if you wish so after my other PRs are merged.

Unclear how to use `PaperTrail.Multi`.

We're trying to integrate PaperTrail in our application, which uses Ecto.Multi quite extensively.
We've tried many different combinations of PaperTrail.Multi.new(), then adding steps using the standard Ecto.Multi.insert and / or Ecto.Multi.update functions, then trying to commit using PaperTrail.Multi.commit but it doesn't seem to work.
We've noticed that PaperTrail.Multi.insert and PaperTrail.Multi.update exist, but we are confused on the exposed API here - these two don't expose a "step name" for example - so we're not sure whether they're drop-in replacement or wrappers, and how to use them.

It would be great if someone could indicate how to use PaperTrail with Multi - or maybe add tests so that we could understand?

So far, the only solution we found would be to replace all our calls to Multi.update / Multi.insert by Multi.run wrapping PaperTrail.* equivalents... Is it the way to go?

First version for existing records.

I'm trying to implement paper_trail in my project. How can I add the first version to existing records? Is it possible? Or this functionality is only available for new records?

Multi-Repo Configuration

Currently all configuration is applied globally. To enable things like #128, this library should be configurable per Repo and not globally.

Originator relationship not working

I can see that I'm storing the originator_id and I have the config set to use "user" correct as I can use the name user for the origininator

IE |> PaperTrail.insert(user: user) and again I can see the id being set correctly in the db.

But when I try to preload or call upon the originator thats where I hit a problem.


{:ok, results} = cs |> PaperTrail.insert(user: user)

version = results[:version]

version |> IO.inspect
 --->
%PaperTrail.Version{
  __meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
  event: "insert",
  id: 12,
  inserted_at: ~N[2018-02-14 19:12:08.399937],
  item_changes: %{
   ...
  },
  item_id: 31,
  item_type: "Client",
  meta: nil,
  origin: nil,
  originator_id: 1
}

version.user
** (KeyError) key :user not found in: %PaperTrail.Version{...}

version.originator
** (KeyError) key :originator not found in: %PaperTrail.Version{...}

version.originator_id
--> 1

version |> Repo.preload(:user)
** (ArgumentError) schema PaperTrail.Version does not have association :user

version |> Beffect.Repo.preload(:originator)
** (ArgumentError) schema PaperTrail.Version does not have association :originator

Not sure what I'm missing here.

Spec:

  • Elixir 1.6.0
  • Phoenix 1.3
  • ecto 2.2.8
  • paper_trail 0.7.7

Encoding problems with many_to_many for PaperTrail.update/2

So I'm doing something like

ids = [1,2,3]
items = MyApp.Item |> where([p], p.id in ^ids) |> Repo.all
changeset = Ecto.Changeset.put_assoc(changeset, :items, items)

PaperTrail.update changeset

Now, when saving, it gives me the following error:
** (Poison.EncodeError) unable to encode value: {:assoc, %Ecto.Association.BelongsTo{cardinality: :one, defaults: [], field: :something, on_cast: nil, on_replace: :raise, owner: MyApp.Something, owner_key: :something_id, queryable: MyApp.Something, related: MyApp.Something, related_key: :id, relationship: :parent, unique: true}}

I've tried setting a custom encoder for my model/schema in my app with poison, but it doesn't seem to work for paper_trail.

What's the best way to handle this?

Unable to perform update when the changeset also includes changesets from nested assocs

Hi all ๐Ÿ‘‹

I'm having some issues when performing PaperTrail.update but everything ends well if I just use Repo.update.

I've no issues with the insert:

  def create_incident(attrs \\ %{}) do
    %Incident{}
    |> Incident.changeset(attrs)
    |> put_assoc(:users, Users.get_users_by_id(attrs["users"]))
    |> PaperTrail.insert(originator: attrs["user"])
  end

changeset:

#Ecto.Changeset<
  action: nil,
  changes: %{
    started_at: ~U[2016-01-01 00:00:00Z],
    ...
    users: [
      #Ecto.Changeset<action: :update, changes: %{}, errors: [],
       data: #IncidentsApi.Users.User<>, valid?: true>
    ]
  },
  errors: [],
  data: #IncidentsApi.Incidents.Incident<>,
  valid?: true
>
schema "incidents" do
   field :started_at, :utc_datetime
   ...

   many_to_many :users, User, join_through: UserIncident, on_replace: :delete

   has_many :versions, PaperTrail.Version, foreign_key: :item_id, where: [item_type: "Incident"]

   timestamps()
 end

 @doc false
 def changeset(incident, attrs) do
   incident
   |> cast(attrs, [
     :started_at,
     ...
   ])
   |> validate_required([
     :started_at,
     ...
   ])
end

but when I try the update:

  def update_incident(%Incident{} = incident, attrs) do
    incident
    |> Incident.changeset(attrs)
    |> put_assoc(:users, Users.get_users_by_id(attrs["users"]))
    |> PaperTrail.update(originator: attrs["user"])
  end

ends in:
** (Protocol.UndefinedError) protocol Jason.Encoder not implemented for #Ecto.Changeset<action: :replace, changes: %{}, errors: [], data: #IncidentsApi.Users.User<>, valid?: true> of type Ecto.Changeset (a struct), Jason.Encoder protocol must always be explicitly implemented.

changeset:

#Ecto.Changeset<
action: nil,
changes: %{
  users: [
    #Ecto.Changeset<action: :replace, changes: %{}, errors: [],
     data: #IncidentsApi.Users.User<>, valid?: true>,
    #Ecto.Changeset<action: :update, changes: %{}, errors: [],
     data: #IncidentsApi.Users.User<>, valid?: true>
  ]
},
errors: [],
data: #IncidentsApi.Incidents.Incident<>,
valid?: true
>

The only difference that I'm seeing is the replace changeset. Any idea? Am I doing something wrong or this is a known issue?

btw I know by design PaperTrail does not track nested assoc and I'm okay with it, I just want to perform a successful update even if the changes don't reflect the updated assocs ๐Ÿ‘Œ

Allow strict_mode on a per-model basis

I have an application where I need to use Strict Mode for a certain set of models, but not for all models. I would like to be able to specify if a model should be treated as "strict", or optionally if a model does not have the first_version_id and current_version_id fields, it should not be treated as strict. Currently if a model does not have these fields, it throws:

     ** (ArgumentError) unknown field `current_version_id` in ...

During an insert.

(For now, I may just add these fields to all my models as null: false.)

Is it possible to delay inserting versions while seeding data?

I have seeds, which are inserting lot of various data into DB.

Profiling shows that lot of time is spent inserting paper_trail versions.

Is it possible to postpone inserting paper_trail versions to the end of seeding? And then insert all versions in one batch?

** (UndefinedFunctionError) function Repo.transaction/1 is undefined (module Repo is not available)

I'm trying out the latest 0.7.6 and getting this error when trying to update a record.
The update is a simple PaperTrail.update(changeset, origin: "test")

I did condigure the config :paper_trail, repo: MyApp.Repo and seems to work when I call Papertrail.Version.count().

** (UndefinedFunctionError) function Repo.transaction/1 is undefined (module Repo is not available)
    Repo.transaction(%Ecto.Multi{names: #MapSet<[:model, :version]>, operations: [version: {:run, #Function<10.48676158/1 in PaperTrail.update/2>}, model: {:changeset, #Ecto.Changeset<action: :update, changes: %{name: "First Surname v3"}, errors: [], data: #App.Person<>, valid?: true>, []}]})
    (paper_trail) lib/paper_trail.ex:151: PaperTrail.update/2

From iex console, calling PaperTrail.RepoClient.repo returns the correct Repo.
So the configuration should not be the problem.

PaperTrail.update fails at setting the item_changes for versions when struct include nested ecto changeset for assoc

when trying to update a valid changeset I get some odd errors the call out an issue on the associated recorders.

Note: the changeset is valid.

client |> Client.changeset(attrs)
#Ecto.Changeset<
  action: nil,
  changes: %{
    first_name: "Test",
    phone_numbers: [
      #Ecto.Changeset<
        action: :insert,
        changes: %{notes: "asdf", number: "555-555", type: "Home"},
        errors: [],
        data: #Beffect.Intake.Client.PhoneNumber<>,
        valid?: true
      >
    ]
  },
  errors: [],
  data: #Beffect.Intake.Client<>,
  valid?: true 
>

pry(4)> client |> Client.changeset(attrs) |> PaperTrail.update(originator: user)
iex(1)> [debug] QUERY OK db=0.2ms
begin []
[debug] QUERY OK db=5.0ms
UPDATE "clients" SET "first_name" = $1, "updated_at" = $2 WHERE "id" = $3 ["Test", {{2018, 2, 14}, {23, 44, 15, 517011}}, 27]
[debug] QUERY OK db=2.0ms
INSERT INTO "client_phone_numbers" ("can_message","client_id","notes","number","preferred","type","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id" [false, 27, "asdf", "555-5555", false, "Home", {{2018, 2, 14}, {23, 44, 15, 525806}}, {{2018, 2, 14}, {23, 44, 15, 525815}}]
[debug] QUERY ERROR db=8.7ms
INSERT INTO "versions" ("event","item_changes","item_id","item_type","originator_id","inserted_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id", "origin" ["update", %{first_name: "Test", phone_numbers: [#Ecto.Changeset<action: :insert, changes: %{notes: "asdf", number: "555-5555", type: "Home"}, errors: [], data: #Beffect.Intake.Client.PhoneNumber<>, valid?: true>]}, 27, "Client", 1, {{2018, 2, 14}, {23, 44, 15, 530146}}]
[debug] QUERY OK db=0.9ms
rollback []
** (Poison.EncodeError) unable to encode value: {nil, "client_phone_numbers"}
    (poison) lib/poison/encoder.ex:383: Poison.Encoder.Any.encode/2
    (poison) lib/poison/encoder.ex:227: anonymous fn/4 in Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:227: anonymous fn/4 in Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:227: anonymous fn/4 in Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:259: anonymous fn/3 in Poison.Encoder.List.encode/3
    (poison) lib/poison/encoder.ex:260: Poison.Encoder.List.encode/3
    (poison) lib/poison/encoder.ex:227: anonymous fn/4 in Poison.Encoder.Map.encode/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
    (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map.encode/3
    (poison) lib/poison.ex:41: Poison.encode!/2
    (ecto) /Users/joshchernoff/Dev/elixir/beffect/deps/postgrex/lib/postgrex/type_module.ex:717: Ecto.Adapters.Postgres.TypeModule.encode_params/3
    (postgrex) lib/postgrex/query.ex:45: DBConnection.Query.Postgrex.Query.encode/3
    (db_connection) lib/db_connection.ex:1079: DBConnection.describe_run/5
    (db_connection) lib/db_connection.ex:1150: anonymous fn/4 in DBConnection.run_meter/5

heres how I build the assoc

has_many(
      :phone_numbers,
      Beffect.Intake.Client.PhoneNumber,
      on_delete: :delete_all
    )

I'm not doing much other than just simply
|> cast_assoc(:phone_numbers)
with my change set

vars:

client =

%Beffect.Intake.Client{
  __meta__: #Ecto.Schema.Metadata<:loaded, "clients">,
  phone_numbers: [],
  ...
}

attrs =

%{
  ...
  "phone_numbers" => %{
    "0" => %{"notes" => "asdf", "number" => "555-5555", "type" => "Home"}
  }
}

PaperTrail.Version fields are tagged read_after_write - MySQL does not support

getting this error after installing PaperTrail 1.0.0 for first time:

(ArgumentError) MySQL does not support :read_after_writes in schemas for non-primary keys. The following fields in PaperTrail.Version are tagged as such: [:id, :origin]

OTP 26 with Elixir 1.15.4 using MySQL via MyXQL

Thoughts on how to avoid this? :id I believe IS my primary key.

How to revert back to anytime in history? How to merge versions?

Hi, The documentation mentions that paper_trail can "revert back to anytime in history", but I can't figure out exactly how to revert or reconstitute an older version of a model. Is there documentation somewhere that describes this?

Also, it's mentioned that you "don't delete your paper_trail versions, instead you can merge them", but I can't figure out exactly how to merge them. Is there documentation somewhere that describes this?

Can I use Papertrail.Multi.insert with Enum.reduce?

The Readme for the project says it's cool to ask questions in a new GH Issue; if you'd prefer this on a forum instead let me know.

I refactored my code from a Repo.insert_all to Enum.reduce -> Ecto.Multi.insert so I can move it to Papertrail to track the inserts. This works:

{:ok, inserted} =
  Enum.reduce(foos_attrs, Ecto.Multi.new(), fn attrs, multi ->
    Ecto.Multi.insert(
      multi,
      {:foo, foo.id},
      Foo.changeset(%Foo{}, attrs)
    )
  end)
  |> Repo.transaction()

But it looks like PaperTrail.Multi.insert/3 doesn't take a name argument like Ecto.Multi.insert/4 does.

This results in the error:
** (RuntimeError) :model is already a member of the Ecto.Multi:

when doing this in the reduce:

PaperTrail.Multi.insert(
  multi,
  Foo.changeset(%Foo{}, attrs)
)

Is there another way to do this that I missed? Thanks.

Nested association tracking after version 0.9.0

Hello, this is more of a question regarding versioning and nested associations.

Before version 0.10.X we were able to track nested association changes out of the box, referencing has_many/belongs_to etc. associations on changesets.

The last successful version this worked for was 0.9.0

Was this an intentional change? I scanned the changelog and was not able to find any information on if removing trails on Ecto associations was done intentionally, or if this is a bug.

I know there was a lot of discussion on nested associations generating/being included in a paper trail, where this does work for embeds.

Any assistance here is appreciated!

Allow Model ID column to be configurable.

I use UUIDs for my models with PostgreSQL (though the database layer shouldn't matter here). At this point in the insert bits, it looks like it's hardcoded to use id as the attribute.

I'm proposing to have a id_column configuration option that allows for specifying this.

Side-note: this looks like a prime spot for some refactoring re: how that Version struct is built!

Elixir 1.6.5 - Compilation warnings result in "module Papertrail is not available"

When compiling my app, I see these warnings:

Compiling 5 files (.ex)
warning: clauses for the same def should be grouped together, def get_version/2 was previously defined (lib/paper_trail/version_queries.ex:61)
  lib/paper_trail/version_queries.ex:90

warning: function MyApp.Repo.all/1 is undefined (module MyApp.Repo is not available)
Found at 2 locations:
  lib/paper_trail/version_queries.ex:32
  lib/paper_trail/version_queries.ex:48

warning: function MyApp.Repo.get/2 is undefined (module MyApp.Repo is not available)
  lib/paper_trail/version_queries.ex:99

warning: function MyApp.Repo.one/1 is undefined (module MyApp.Repo is not available)
Found at 2 locations:
  lib/paper_trail/version_queries.ex:76
  lib/paper_trail/version_queries.ex:92

Finally, when attempting to use Papertrail in my app, I get the following error:

** (UndefinedFunctionError) function Papertrail.insert/2 is undefined (module Papertrail is not available)

Note; MyApp is a placeholder for what my app is actually called.

Am I missing something obvious?

paper_trail == 0.7.7
ecto == 2.2.8
postgrex == 0.13.5

elixir 1.6.5

Models with the same aliased name are pulling each others version history

This is more of a question, but I'm running into a case where we have 2 models, say:

  • Myapp.SomeModule.Category
  • Myapp.SomeOtherModule.Category

Both of these are showing up in the versions table as item_type: "Category", and if the id's are the same, they pull each other's version history.

Is there a way to use the fully qualified name in the item_type column of the versions table, or to specify what I would like to use as the item_type?

Any assistance is appreciated!

Question: Is it possible to insert the version and change the model using 2 steps in the database?

Hello, thank you very much for the awesome library!

I was just wondering if it is possible to insert the version record in the database first, and then, in a second stage update the model...
(is there any consistency checks assumptions that the library relies on and that would break?). Have anyone ever tried to do that?

This question might sound weird, but the use case is rather simple: imagine an application that regular users can change a record, but the changes need to be approved by a moderator... (I understand that multiple users proposing changes at the same is problematic, but for the sake of simplicity, let's assume each record belongs to a single user, only the owner can "propose changes", and that owners cannot propose new changes while there are pending changes).

Would it be the case of creating the version using PaperTrail.make_version_struct, inserting it to the database via conventional Repo.insert and then later (when the change is approved) creating a new changeset for the model using PaperTrail.Version.item_changes and updating the model via conventional Repo.update?

Initialise already existing records

Hi, would you accept a PR for creating an entry in the version table for records that have been inserted without paper_trail?

My use case is that I have many records that were inserted before we adopted your library, now I would like to be able to track future changes so I was thinking of creating PaperTrail.initialise/2 function and apply it to all existing records.

Version originator relationships

This is the documentation

in your config/config.exs

config :paper_trail, originator: [name: :user, model: YourApp.User]

Let's say my model is App.Account.User
I need to config

config :paper_trail, originator: [name: :user, model: App.Account.User]

Technically, It should get the association with User but It got Account.
To make it work, I have to create one more model with the name App.User but I don't think this is a good way.

Can you help take a look at it?

Thanks for your supporting

Strict Mode with UUID

Currently Strict Mode is not supported when using UUIDs.

It could however easily be supported by generating the UUIDs on the client side.

This could look something like this:

multi
|> Ecto.Multi.run(initial_version_key, fn repo, %{} ->
  version_id = get_sequence_id("versions") + 1

  changeset_data =
    Map.get(changeset, :data, changeset)
    |> Map.merge(%{
      uuid: Ecto.UUID.generate(),
      first_version_id: version_id,
      current_version_id: version_id
    })

  initial_version = make_version_struct(%{event: "insert"}, changeset_data, options)
  repo.insert(initial_version)
end)
|> Ecto.Multi.run(model_key, fn
  repo, %{^initial_version_key => initial_version} ->
    updated_changeset =
      changeset
      |> change(%{
        uuid: initial_version.item_changes["uuid"],
        first_version_id: initial_version.id,
        current_version_id: initial_version.id
      })

    repo.insert(updated_changeset, ecto_options)
end)

Using PaperTrail with EctoConditionals (Interoperability)

I am using PaperTrail to track versioning in conjunction with EctoConditionals.

https://hex.pm/packages/ecto_conditionals
https://github.com/codeanpeace/ecto_conditionals

The documentation mentions that a Repo should be passed in:

use EctoConditionals, repo: MyApp.Repo

This allows calls such as:

%User{id: 1, name: "Flamel"} |> find_or_create
#=> {:ok, %User{id: 1, name: "Flamel"}}

The only problem is that this will not call PaperTrail.insert!/1.

I managed to workaround this by creating a delegator module and passing it into EctoConditionals as below:

use EctoConditionals, repo: PaperTrail.Delegator

This then forwards function calls to PaperTrail. Here is the code:

defmodule PaperTrail.Delegator do
  defdelegate get_version(record), to: PaperTrail.VersionQueries
  defdelegate get_version(model_or_record, id_or_options), to: PaperTrail.VersionQueries
  defdelegate get_version(model, id, options), to: PaperTrail.VersionQueries
  defdelegate get_versions(record), to: PaperTrail.VersionQueries
  defdelegate get_versions(model_or_record, id_or_options), to: PaperTrail.VersionQueries
  defdelegate get_versions(model, id, options), to: PaperTrail.VersionQueries
  defdelegate get_current_model(version), to: PaperTrail.VersionQueries

  defdelegate insert(changeset, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]), to: PaperTrail
  defdelegate insert!(changeset, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]), to: PaperTrail
  defdelegate update(changeset, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]), to: PaperTrail
  defdelegate update!(changeset, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]), to: PaperTrail
  defdelegate delete(struct, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]), to: PaperTrail
  defdelegate delete!(struct, options \\ [origin: nil, meta: nil, originator: nil, prefix: nil]), to: PaperTrail
  defdelegate get_model_id(model), to: PaperTrail

  defdelegate get_by(queryable, clauses, opts \\ []), to: MyApp.Repo
  defdelegate get_by!(queryable, clauses, opts \\ []), to: MyApp.Repo
  defdelegate get(queryable, id, opts \\ []), to: MyApp.Repo
  defdelegate get!(queryable, id, opts \\ []), to: MyApp.Repo
end

Please note that the delegate list is incomplete,. It would also need to respect the @repo configuration specified in config/config.exs. I can make a pull request with this functionality if you would like.

I am not sure if this is a good solution or not, so I would appreciate any feedback. It seems a little bit hacky to me, but it worked in my situation. It may be worth also raising this issue with the author of EctoConditionals to share ideas on how to approach interoperability issues between helper libraries such as that.

Settings does not work in application with runtime

Hello! I'm using paper_trail in an application when code is compiled, and the settings of lib do not work.

This error is returned when a try to save, update or delete data with PaperTrail:
** (Ecto.ChangeError) value `"9002cb70-ddf1-4c9b-8c88-458f3086d14d"` for `PaperTrail.Version.item_id` in `insert` does not match type :integer

When I start the application with mix phx.server the settings works and the error does not happen.

That is my config in runtime.exs:

config :paper_trail,
    repo: BalanceManager.Repo,
    item_type: Ecto.UUID,
    originator_type: Ecto.UUID,
    originator_relationship_options: [references: :uuid],
    strict_mode: false

This is my versions migrate:

defmodule Repo.Migrations.AddVersionsPaperTrail do
  use Ecto.Migration

  def change do
    create table(:versions) do
      add :event, :string, null: false, size: 10
      add :item_type, :string, null: false
      add :item_id, :binary_id
      add :item_changes, :map, null: false
      add :originator_id, :binary_id
      add :origin, :string, size: 50
      add :meta, :map

      # Configure timestamps type in config.ex :paper_trail :timestamps_type
      add :inserted_at, :utc_datetime, null: false
    end

    create index(:versions, [:originator_id])
    create index(:versions, [:item_id, :item_type])
    # Uncomment if you want to add the following indexes to speed up special queries:
    # create index(:versions, [:event, :item_type])
    # create index(:versions, [:item_type, :inserted_at])
  end
end

Can someone help me?

Is there a way to pass originator_id instead of originator struct?

First off, nice repo very helpful!

Use case:
I have the user_id inside a Merchant struct (belongs_to) and most of the times the user isn't loaded but I do have the originator_id (user_id) available. It would be better to not preload an association that is only going to use the .id when passing the originator: struct

[Feature Request / Looking for Solution] How to utilize Ecto's Multi + Create Version

We are trying to find a way on how to wrap multiple updates on a transaction + build the version but not sure what would be the right approach.
We have the following scenario:

I have a user and address schema.
A user may have many addresses.
When someone updates their address, i would also like to update the user's 'last_updated_at' date field (in the user table).
We currently have wrapped both updates on a Multi and they will be executed in a transaction.
Now we want to introduce versioning on the User Model but we see 1-2 ways that can be done:

  1. Get access to private function make_version_struct (https://github.com/izelnakri/paper_trail/blob/master/lib/paper_trail.ex#L295 ) and add it on our existing multi (bypassing all other paper_trail logic which can not guarantee future-compatibility)
  2. Implement something like PaperTrail.multi? (that means that we need to provide which changesets are to be stored as versions or if the whole thing will be treated as one, etc)

Add multi tenancy capabilities with Ecto meta prefix

Sometimes it's required to work on a multi tenancy system, and be able to
keep track of the versions of the records/models on different schemas/databases. PaperTrail
doesn't allow this at this moment.

Expected behavior

Be able to specify the tenant where the versions entries generated by the user's activity will be stored.

Current behavior

Now it's not possible.

Possible solution

Leverage on the Ecto.Query prefix in order to switch between different schemas/databases. It's necessary to let the user specify this value from the options of the functions.

Tracking current_version_id in item_changes seems superfluous.

Tracking current_version_id in item_changes seems superfluous. It is best in many circumstances to deal with a dataset that only highlights what actually changed. Particularly if trying to building a history or timeline of changes to a record in the UI or comparing versions of a record. Having metadata such as current_version_id can be tiresome, since it needs to be filtered out.

It is also not particularly useful as it is just a reference to itself and can be inferred from versions.id in any case.

image

item_changes:
{
    "user_id": 2,             <-- the actual change
    "current_version_id": 5   <-- superfluous
}

What would be a common use case of paper_trail?

Hi, does this library allow you to do like the "undo" feature in Google documents and where you have access to all the modification history?

Or is it more devops focused? for example by reversing to previous database values if something goes wrong

Thanks

Filter Changes

Is there interest in being able to filter certain changes from the changes stored in the DB? Suppose I have a User schema. I might not want to version their password hash, for example. This could perhaps be done as a hook in the make_version_struct or serialize functions. I can see use cases for having filters defined at the schema level, as well as individual inserts/updates, so being able to pass in a list of fields to omit would be ideal. Having :only/:except opts would be consistent with other Elixir code in the core lib.

I'm imagining something like like:

user = User.changeset(%User{}, %{password: "hash", first_name: "John", last_name: "Smith"})
PaperTrail.insert!(user, only: [:first_name, :last_name]) 
PaperTrail.insert!(user, except: [:password])  

defimpl PaperTrail.ChangeFilter.Only, for: User do
  def only(), do: [:first_name, :last_name]
end

defimpl PaperTrail.ChangeFilter.Except, for: User do
  def except(), do: [:password]
end

I imagine if both are defined, only wins over except and the intersection of defimpl except opt except and the union of defimpl only and opt only, though I'm not sure how complicated those rules should really be.

Update Changelog

A Changelog is crucial for assessing whether to upgrade, and what changes might be required in a user's codebase to facilitate the upgrade.

Ideally under each version it will categorize the types of changes:

  • Enhancements
  • Changes
  • Bug fixes
  • Deprecations
  • Breaking changes

Multiple originator models

Hi,

thank you for this amazing library! In my application, there are multiple types of resources that I manage, App.TypeA, App.TypeB etc. Some of these resources can have a App.UserAccount, where the resource is the actual user. What would be the best way to store the originator? I thought about using the originator like $type:$id, but actual references would be preferred.

Thank you!

Problem updating changesets with relations.

I'm trying to use PaperTrail.update to update a record that has a has_many relation, but it is triggering the next error:

web_1  | #Ecto.Changeset<action: nil,                                                                                                          [4/4594]
web_1  |  changes: %{current_status: 49,
web_1  |    key_results: [#Ecto.Changeset<action: :update,
web_1  |      changes: %{values: %{"current_value" => 49, "desired_value" => 100}},
web_1  |      errors: [], data: #Performancex.KeyResult<>, valid?: true>]}, errors: [],
web_1  |  data: #Performancex.Objective<>, valid?: true>
web_1  | [debug] QUERY OK db=1.3ms
web_1  | begin []
web_1  | [debug] QUERY OK db=2.2ms
web_1  | UPDATE "objectives" SET "current_status" = $1, "updated_at" = $2 WHERE "id" = $3 [49, {{2017, 5, 4}, {10, 40, 57, 718198}}, 1]
web_1  | [debug] QUERY OK db=0.4ms
web_1  | UPDATE "key_results" SET "values" = $1, "updated_at" = $2 WHERE "id" = $3 [%{"current_value" => 49, "desired_value" => 100}, {{2017, 5, 4}, {1
0, 40, 57, 720656}}, 1]
web_1  | [debug] QUERY ERROR db=5.3ms
web_1  | INSERT INTO "versions" ("event","item_changes","item_id","item_type","originator_id","inserted_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id",
 "origin" ["update", %{current_status: 49, key_results: [#Ecto.Changeset<action: :update, changes: %{values: %{"current_value" => 49, "desired_value" =
> 100}}, errors: [], data: #Performancex.KeyResult<>, valid?: true>]}, 1, "Objective", 2, {{2017, 5, 4}, {10, 40, 57, 721334}}]
web_1  | [debug] QUERY OK db=0.1ms
web_1  | rollback []
web_1  | [info] Sent 500 in 53ms
web_1  | [error] #PID<0.644.0> running Performancex.Endpoint terminated
web_1  | Server: localhost:4000 (http)
web_1  | Request: PUT /api/v1/performance/objectives/1
web_1  | ** (exit) an exception was raised:
web_1  |     ** (Poison.EncodeError) unable to encode value: {nil, "key_results"}
web_1  |         (poison) lib/poison/encoder.ex:354: Poison.Encoder.Any.encode/2
web_1  |         (poison) lib/poison/encoder.ex:213: anonymous fn/4 in Poison.Encoder.Map.encode/3
web_1  |         (poison) lib/poison/encoder.ex:214: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
web_1  |         (poison) lib/poison/encoder.ex:214: Poison.Encoder.Map.encode/3
web_1  |         (poison) lib/poison/encoder.ex:213: anonymous fn/4 in Poison.Encoder.Map.encode/3
web_1  |         (poison) lib/poison/encoder.ex:214: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
web_1  |         (poison) lib/poison/encoder.ex:214: Poison.Encoder.Map.encode/3
web_1  |         (poison) lib/poison/encoder.ex:213: anonymous fn/4 in Poison.Encoder.Map.encode/3
web_1  |         (poison) lib/poison/encoder.ex:214: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
web_1  |         (poison) lib/poison/encoder.ex:214: Poison.Encoder.Map.encode/3
web_1  |         (poison) lib/poison/encoder.ex:232: anonymous fn/3 in Poison.Encoder.List.encode/3
web_1  |         (poison) lib/poison/encoder.ex:233: Poison.Encoder.List.encode/3
web_1  |         (poison) lib/poison/encoder.ex:213: anonymous fn/4 in Poison.Encoder.Map.encode/3
web_1  |         (poison) lib/poison/encoder.ex:214: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
web_1  |         (poison) lib/poison/encoder.ex:214: Poison.Encoder.Map.encode/3
web_1  |         (poison) lib/poison.ex:41: Poison.encode!/2
web_1  |         (ecto) /performancex/deps/postgrex/lib/postgrex/type_module.ex:717: Ecto.Adapters.Postgres.TypeModule.encode_params/3

It works fine with Repo.update so, do you have any idea that could be happening?

(FunctionClauseError) no function clause matching in Regex.safe_run/3 on mix deps.get

Getting this error when trying to get the dependencies
`
** (FunctionClauseError) no function clause matching in Regex.safe_run/3

The following arguments were given to Regex.safe_run/3:

    # 1
    ~r/\/\/([^:]*):[^@]+@/

    # 2
    "https://repo.hex.pm/tarballs/phoenix-1.4.9.tar"

    # 3
    [{:capture, :all, :index}, :global]

Attempted function clauses (showing 1 out of 1):

    defp safe_run(%Regex{re_pattern: compiled, source: source, re_version: version}, string, options)

(elixir) lib/regex.ex:463: Regex.safe_run/3
(elixir) lib/regex.ex:663: Regex.do_replace/4
(hex) lib/hex/scm.ex:109: Hex.SCM.update/1
(hex) lib/hex/scm.ex:176: Hex.SCM.checkout/1
(mix) lib/mix/dep/fetcher.ex:64: Mix.Dep.Fetcher.do_fetch/3
(mix) lib/mix/dep/converger.ex:190: Mix.Dep.Converger.all/9
(mix) lib/mix/dep/converger.ex:123: Mix.Dep.Converger.all/7
(mix) lib/mix/dep/converger.ex:108: Mix.Dep.Converger.all/4

`

Feature Request: Current values

what do you think about adding the possibility to register the pre-change in the versions?

with this feature we:

  1. Direct show changes instead of load all previous versions at find last change. Which is even more critical with multiple fields.
  2. can search specific changes like: task status updated from TODO to DONE

i believe this feature can be added without changing the current lib behavior

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.