I store my application data in a DB (using Ecto).
I’d like to implement a (basic) undo/redo.
Has anybody ever done this? Any infos?
This is easy for simple changes.
defmodule Undo do
use Agent
import Ecto.Changeset
# TODO get rid of this
alias MyProject.Repo
def start_link() do
Agent.start_link(fn -> [] end, name: __MODULE__)
end
def undoable_change(data, changes) do
changeset = change(data, changes)
push(%{
schema: data.__meta__.schema,
ref_id: data.id,
changes: Map.take(data, Map.keys(changeset.changes))
})
changeset
end
def undo() do
case pop() do
{:ok, undo_info} ->
data = undo_info.schema |> Repo.get!(undo_info.ref_id)
changeset = change(data, undo_info.changes)
Repo.update!(changeset)
{:no_undo_info, _} ->
:no_undo_info
end
end
def pop do
Agent.get_and_update(__MODULE__, fn
[undo_info | undo_stack] -> {{:ok, undo_info}, undo_stack}
[] -> {:no_undo_info, []}
end)
end
def push(undo_info) do
Agent.update(__MODULE__, &[undo_info | &1])
end
end
But I’m not sure how to handle references.
Obviously I can’t just delete child-records if I delete a record.
Maybe just flag a record as deleted. But then I may have to manually flag childs.
Undo / Redo is basically never basic, especially when you’re doing it across multiple records.
Generally the traditional answer is to use an event system. You cut an event which represents a logical action, and then that event triggers changes to DB tables. Then you create an invert function that basically takes an event, and cuts a new event that is the logical inverse. Then that event triggers general changes to the db. Redo is just invert(invert(event))
You can’t meaningfully implement an undo as a simple “put the database back how it was” because you can’t undelete records, and you can’t simply put foreign keys back the way they were.
Yes, I thnk changeset should be a good starting point for that.
As you see its simple for basic changes.
Just wondering about the references. Thought that so would have done that with Ecto already.
Here’s the issue with changesets: they aren’t persisted. Suppose you have a record, and you make a change via a changeset to even just that one record. Then, you want to initiate an “undo”. How do you call invert(changeset) ? That changeset is gone.
Whats wrong about the solution I posted using the Agent? I think it should work for changes not involving references. But after thinking about it a little more, I think it really gets messy with references.
I’ve thought of authoring something similar years ago but I’ve already used the library in the past and it’s working pretty well. Though I can’t remember if it works with Ecto.Multi, the docs only reference Ecto.Repo.
I think my confusion with the agent as a persistence mechanism is a lack of clarity about how it would interface with end users. Are you expecting that the end user is using LiveView so that you can pair the agent with a given live view? If they navigate away, do they lose the ability to undo / redo?
Sorry, I don’t understand. I’m asking how your code would combine a user interface where people do and undo things, with your Undo agent, from a process architecture standpoint.
Well they don’t use that language but “reverting a model to a previous state” or “tracking changes” and letting you revert then kind of speaks to that I think.
I’m at the very beginning of this, and did not think that far. I’m happy for now when I can change stuff in the DB using iex and undo/redo it afterwards.
After looking into this a little more, I decided not to use a DB as Application-file-format, but libgraph. Its a prefect fit for what I need and undo/redo I can simply do with
Its a kind of boring tool that automatically connects devices with a number of ports of maybe different functionality with other devices with matching ports. I can’t decide if I can show code as its not mine.