From 8e424cfcee54daaa1bb761f977154871ed4bad1c Mon Sep 17 00:00:00 2001 From: Izel Nakri Date: Tue, 14 Mar 2017 03:10:00 +0100 Subject: [PATCH] major documentation change --- README.md | 117 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e17221fa..21f0ddb8 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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, # 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): @@ -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) @@ -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 @@ -90,12 +90,12 @@ 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 @@ -103,7 +103,7 @@ The library source code is minimal and tested. It is highly suggested that you c ```elixir def deps do - [{:paper_trail, "~> 0.5.0"}] + [{:paper_trail, "~> 0.6.0"}] end ``` @@ -111,6 +111,7 @@ The library source code is minimal and tested. It is highly suggested that you c ```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: @@ -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 @@ -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 @@ -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 @@ -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