Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

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

Open
jesseshieh opened this issue Apr 20, 2017 · 9 comments
Open

Comments

@jesseshieh
Copy link

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?

@izelnakri
Copy link
Owner

izelnakri commented Apr 22, 2017

Hi @jesseshieh,

Yes PaperTrail can revert back to anytime in history because the data is already there. item_changes column holds all the changes that occured in a single version. In order revert back in history you need to start from the beginning(because we dont want to store/duplicate data in each version), the initial version. You should also need to account relationships(related model changes) and create an additional version once your operation is complete.

If you want to merge versions you should pay attention to your version meta_tags, ideally you wouldn't want to lose them. Our focus has been on building the foundation(the initial data storage) so far.

Currently there isn't a single function for these that ships with the library. All features are heavily tested before release. Also I'm not reverting or merging versions in any of my applications currently. Therefore If you are willing to contribute by adding these functionalities I'll be happily review and collaborate.

Thanks for your interest in the project!

@ku1ik
Copy link

ku1ik commented Aug 30, 2018

This is one way of doing it:

  def travel(post, time) do
    post
    |> PaperTrail.get_versions()
    |> Enum.sort_by(& &1.id)
    |> Enum.take_while(& &1.inserted < ^time)
    |> Enum.map(& &1.item_changes)
    |> Enum.reduce(%{}, &Map.merge(&2, &1))
  end

The result is a map with the attributes post had at a given time. This could be further piped into Ecto.Changeset.cast and Ecto.Changeset.apply_changes to get actual struct:

  def travel(post, time) do
    attrs =
      post
      |> PaperTrail.get_versions()
      |> Enum.sort_by(& &1.id)
      |> Enum.take_while(& &1.inserted < ^time)
      |> Enum.map(& &1.item_changes)
      |> Enum.reduce(%{}, &Map.merge(&2, &1))

    %Post{}
    |> Ecto.Changeset.cast(attrs, [Map.keys(attrs)])
    |> Ecto.Changeset.apply_changes()
  end

@tomconroy
Copy link

tomconroy commented Sep 15, 2018

Is there a story for tracking how the shape of a model might change over its lifetime? For example, if a field name is changed, or a new (required) field is added. Or a more complex example, a column is extracted to a new table, and replaced with a reference?

In these cases, it seems difficult to revert back to an old version. Are there any examples of this?

@izelnakri
Copy link
Owner

Yes, I thought of implementing something like this @tomconroy, then thought it should be left to the developer during their migration instead. Because checking such changes during each migration with PaperTrail would take too much time and work from PaperTrail side.

@tomconroy
Copy link

Thanks @izelnakri, so do you imagine the developer would migrate the historical versions, rather than implement the different ways a historical version might be restored to the latest schema? Seems like either path could have many complications

@tensiondriven
Copy link

For the benefit of others; something I've been doing is stuffing a serialized representation of an object into the "meta" field on every update. That way, I've got the struct at a given point-in-time and don't have to reconstitute it. This is a naive approach, and I like it because its simple. I cheat a little by updating the record in the Versions table. Something like:

      def insert(changeset, %User{} = user) do
        pt_result = PaperTrail.insert(changeset, originator: user)

        case pt_result do
          {:ok, model_and_version} ->
            model = model_and_version.model
            version = model_and_version.version
            PaperTrail.Version.changeset(version, %{meta: model}) |> Repo.update!()

          {:error, error} ->
            IO.inspect(error)
            raise "Error inserting: #{error}"

          _ ->
            nil
        end

@izelnakri
Copy link
Owner

I'm considering to cut a v1 release after new years. So far I think it would be great if we describe this in the README.md, however if someone wants to create a PR for it, I'm willing to review it.

@tensiondriven
Copy link

@izelnakri What aspect of this would you like to see described? If its the naive approach I posted above, I'd be happy to whip up a PR. For the other approach of building a representation from history, I'm less stoked as any missing rows from the papertrail_versions table would result in an incorrect representation.

@izelnakri
Copy link
Owner

I think naive approach could add to the complexity in terms of maintainance, however I dont mind having a PapertTrail.revert($paperTrailStruct) that can fetch all the versions and assign them one by one to the latest one in the database, maybe 2 function, one to view the built struct, the other to replace existing record with the built struct. Yes I'm aware missing row would result in incorrect representation however we warn the developers in the README.md asking them to merge versions instead of deleting them. Thus I think this would be the best way forward, also ideally we should do this in SQL.

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests

5 participants