Skip to content

Commit

Permalink
major documentation change
Browse files Browse the repository at this point in the history
  • Loading branch information
izelnakri committed Mar 14, 2017
1 parent d2284a2 commit 8e424cf
Showing 1 changed file with 80 additions and 37 deletions.
117 changes: 80 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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/1```, ```PaperTrail.update/1``` or ```PaperTrail.delete/1``` 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 tests for each release. Data integrity is an important purpose 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.
PaperTrail is assailed with hundreds of test assertions for each release. Data integrity is an important purpose 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

Expand All @@ -26,7 +26,7 @@ PaperTrail is assailed with tests for each release. Data integrity is an importa
# 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: #Ecto.DateTime<2016-09-15 21:42:38>,
# updated_at: #Ecto.DateTime<2016-09-15 21:42:38>},
# item_id: 1, item_type: "Post", meta: nil}}}
# 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,
Expand All @@ -50,7 +50,7 @@ PaperTrail is assailed with tests for each release. Data integrity is an importa
# version: %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
# event: "update", id: 2, inserted_at: #Ecto.DateTime<2016-09-15 22:00:59>,
# item_changes: %{title: "Elixir matures fast", content: "Future is already here, you deserve to be awesome!"},
# item_id: 1, item_type: "Post",
# item_id: 1, item_type: "Post", originator_id: nil, originator: nil
# meta: nil}}}

# => on error(it matches Repo.update\2):
Expand All @@ -63,7 +63,7 @@ PaperTrail is assailed with tests for each release. Data integrity is an importa
# %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
# event: "update", id: 2, inserted_at: #Ecto.DateTime<2016-09-15 22:00:59>,
# item_changes: %{title: "Elixir matures fast", content: "Future is already here, you deserve to be awesome!"},
# item_id: 1, item_type: "Post", meta: nil}}}
# item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}}}

updated_post = Repo.get!(Post, 1)

Expand All @@ -79,7 +79,7 @@ PaperTrail is assailed with tests for each release. Data integrity is an importa
# item_changes: %{title: "Elixir matures fast", content: "Future is already here, you deserve to be awesome!",
# id: 1, inserted_at: #Ecto.DateTime<2016-09-15 21:42:38>,
# updated_at: #Ecto.DateTime<2016-09-15 22:00:59>},
# item_id: 1, item_type: "Post", meta: nil}}}
# item_id: 1, item_type: "Post", originator_id: nil, originator: nil, meta: nil}}}

Repo.aggregate(Post, :count, :id) # => 0
Repo.aggregate(PaperTrail.Version, :count, :id) # => 3
Expand All @@ -90,27 +90,28 @@ PaperTrail is assailed with tests for each release. Data integrity is an importa
# item_changes: %{"title" => "Elixir matures fast", content: "Future is already here, you deserve to be awesome!", "id" => 1,
# "inserted_at" => "2016-09-15T21:42:38",
# "updated_at" => "2016-09-15T22:00:59"},
# item_id: 1, item_type: "Post", meta: nil}
# 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 tested. It is highly suggested that you check it out.
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`:

```elixir
def deps do
[{:paper_trail, "~> 0.5.0"}]
[{:paper_trail, "~> 0.6.0"}]
end
```

2. configure paper_trail to use your application repo in `config/config.exs`:

```elixir
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:
Expand All @@ -127,44 +128,60 @@ The library source code is minimal and tested. It is highly suggested that you c

Your application is now ready to collect some history!

## Does this work with phoenix?
#### Does this work with phoenix?

YES! Make sure you do the steps.
YES! Make sure you do the steps above.

## %PaperTrail.Version{} fields:
### %PaperTrail.Version{} fields:

Explain the 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 | Integer | model id of the reference record | Library generates |
| item_changes | Map | all the changes in this version as a map | Library generates |
| originator_id | Integer | 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"}) | Optionally set |
| inserted_at | Date | inserted_at timestamp | Ecto generates |


## Version set_by references:
PaperTrail records have a string field called ````set_by```. PaperTrail.insert/1, PaperTrail.update/1, PaperTrail.delete/1 functions accepts a second argument for the originator. Example:
### Version origin references:
PaperTrail records have a string field called ````origin```. PaperTrail.insert/1, PaperTrail.update/1, PaperTrail.delete/1 functions accepts a second argument to describe the origin of this version. Example:
```elixir
PaperTrail.update(changeset, set_by: "migration")
PaperTrail.update(changeset, origin: "migration")
# or:
PaperTrail.update(changeset, set_by: "user:1234")
PaperTrail.update(changeset, origin: "user:1234")
# or:
PaperTrail.delete(changeset, set_by: "worker:delete_inactive_users")
PaperTrail.delete(changeset, origin: "worker:delete_inactive_users")
```

## Storing setter relationships
You could specify setter relationship to `paper_trail` versions. This is doable by specifying `:setter` keyword list for your application:
### Originator relationships
You can specify setter/originator foreign key relationship to paper_trail versions. By default versions have a nil ```originator_id```. This is doable by specifying `:originator` keyword list for your application configuration:

```elixir
config :paper_trail, setter: [name: :user, model: YourApp.User]
# For most application setter will be user, models can be updated/created/deleted by several users.
# 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.
```

```elixir
Then originator name could be used for querying and preloading however originator setting must be done via originator_id:

```elixir
user = create_user()
PaperTrail.insert(changeset, originator_id: user.id)
{:ok, result} = PaperTrail.update(edit_changeset, originator_id: user.id)
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, originator_id: user.id)
```

# Strict mode
This is a feature more suitable for larger applications where models keep their version references via foreign key constraints. Thus it would be impossible to delete the first and current version of a model. In order to enable this:
This is a feature more suitable for larger applications where models keep their version references via foreign key constraints. Thus it would be impossible to delete the first and current version of a model, makes querying easier more relational database/SQL friendly. In order to enable this:

```elixir
# 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 should be named ```first_version_id```, and ```current_version_id``` in their respective model tables. A tracked model example with a migration file:

```elixir
Expand All @@ -175,6 +192,7 @@ defmodule Repo.Migrations.AddVersions do
add :name, :string, null: false
add :founded_in, :string

# null constraint is optional to make model insertion impossible without a version:
add :first_version_id, references(:versions), null: false
add :current_version_id, references(:versions), null: false

Expand All @@ -187,8 +205,9 @@ defmodule Repo.Migrations.AddVersions do
end

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

import Ecto.Changeset

schema "companies" do
Expand All @@ -197,39 +216,63 @@ defmodule StrictCompany do

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

timestamps()
end

def changeset(struct, params) do
struct
|> cast(params, [:name, :founded_in])
end
end
```

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

```elixir

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: #Ecto.DateTime<2016-09-15 21:42:38>,
# updated_at: #Ecto.DateTime<2016-09-15 21:42:38>, insert_version_id: 1, current_version_id: 1},
# version: %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
# event: "insert", id: 1, inserted_at: #Ecto.DateTime<2016-09-15 22:22:12>,
# item_changes: %{name: "Acme LLC", founded_in: nil, id: 1, inserted_at: #Ecto.DateTime<2016-09-15 21:42:38>},
# originator_id: nil, origin: "unknown", meta: nil}}}
```

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

```elixir

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: #Ecto.DateTime<2016-09-15 21:42:38>,
# updated_at: #Ecto.DateTime<2016-09-15 23:22:12>, insert_version_id: 1, current_version_id: 2},
# version: %PaperTrail.Version{__meta__: #Ecto.Schema.Metadata<:loaded, "versions">,
# event: "update", id: 2, inserted_at: #Ecto.DateTime<2016-09-15 23:22:12>,
# item_changes: %{name: "Acme Inc."}, originator_id: nil, origin: "documentation", meta: nil}}}
```

If the version set_by field isn't provided with a value default set_by be "unknown". Set_by column has a null constraint on strict_mode on purpose, you should really put a set_by to reference who initiated this change in the database.
If the version ```origin``` field isn't provided with a value, default ```origin``` will be "unknown". Origin column has a null constraint on strict_mode by design, you should put an ```origin``` reference to describe who makes the change. This is important for big applications because a model can change from many sources.

## Storing version meta data
You might want to add some meta data that doesnt belong to ``setter_id``, ``set_by`` fields. Such data could be stored in one object name meta in papertrail versions. Meta field could be passed as the second optional parameter to PaperTrail.insert || PaperTrail.update || PaperTrail.delete functions:
### Storing version meta data
You might want to add some meta data that doesn't belong to ``originator_id`` 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:

```elixir

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.id, meta: %{slug: "acme-llc", important: true})
```

## 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

## TODO
## TODO:
** remove wrong Elixir compiler errors
** explain the columns

TODO: update the example, update the code examples, setter relationships

0 comments on commit 8e424cf

Please sign in to comment.