Masu.Write (Masu v0.7.0)

Copy Markdown View Source

A write of a grid's rows: cells edited, new rows, rows deleted or brought back. Every change a person makes to the rows goes through one, whoever saves it.

The grid builds the write, and hands it to your function for its operation (on_update, on_create, on_delete, on_restore on Masu.Components.data_grid/1), which gives it back with a result for each change. Without one, the grid saves it its own way (Masu.Write.save/1: the repo of an Ecto source, or its copy of a list), with changeset.

<.data_grid id="orders" source={Order} repo={Repo} scope={@current_scope}
  on_update={&OrdersGrid.update/1}>

defmodule MyAppWeb.OrdersGrid do
  alias MyApp.Orders

  # A context function, row by row.
  def update(write) do
    Masu.Write.each(write, &Orders.update_order(write.scope, &1.row, &1.params))
  end
end

By the time your function gets it, the grid has done its part: each row is found again through the grid's own query (a person writes only the rows they may see), the cells that do not edit, or that the row's state makes read-only (row_editable, a column's editable), are refused, and so are the cells someone changed meanwhile (see "Two people, one cell" in the Editing guide). A change has only what is left.

What a write has

  • op - :update, :create, :delete or :restore;
  • changes - the Masu.Write.Changes, one for each row;
  • origin - what the person did: :edit (typed in a cell), :paste, :fill, :clear, :undo, :redo (cells); :draft (a new row), :paste (rows of a paste); :rows (the rows of a range), :selection (the rows selected), :redo (a delete again); :undo (rows back); :test from Masu.Test;
  • scope - the grid's scope: the current user, their organization, as your app has it (@current_scope);
  • by - who makes it: on a grid in an Issho room (issho), and for an agent's tools, an Issho.Identity (a person, an agent and whom it acts for), the same as every component's callbacks get; else the grid's user;
  • grid - the id of the grid;
  • ref - the write's reference, for async/1 and Masu.reply/2.

Results

Each change gets one, from the function that writes it:

  • each/2 - a function of each change, {:ok, row} or {:error, reason} (:ok for a delete);
  • all/2 - one function of every change, for a transaction of the whole batch (Ecto.Multi): {:ok, rows}, in the order of the changes, or {:error, reason}, which refuses them all;
  • ok/3 and error/3 - a result for one change;
  • save/1 - the grid's own way, for the changes without a result;
  • async/1 - the changes without a result are saved later, by a job of yours: the grid shows them saving until Masu.reply/2.

A reason is an Ecto.Changeset (its errors go on the cells of its fields), a message (on every cell of the row), or [{key, message}] of column keys. A change left without a result is a mistake, and the grid raises.

The results compose. Your function may save the grid's way and do more, or save each row its own way and the rest the grid's:

def update(write) do
  write
  |> Masu.Write.save()
  |> tap(&Audit.log(&1.scope, Masu.Write.saved(&1)))
end

def update(%Masu.Write{origin: :paste} = write) do
  Masu.Write.all(write, &Orders.update_many(write.scope, &1))
end

def update(write), do: Masu.Write.save(write)

Later: a job

A write that takes long (a paste of 5,000 cells into a system that takes its time, an import of new rows through a queue) can be saved by a job. async/1 says that the changes without a result will be saved; ref reaches the grid from any process (an Oban job, on any node of the cluster):

def update(write) do
  %{ref: write.ref, changes: Enum.map(write.changes, &Map.take(&1, [:id, :params]))}
  |> MyApp.Workers.SaveCells.new()
  |> Oban.insert!()

  Masu.Write.async(write)
end

# In the job:
Masu.reply(ref, :ok)
Masu.reply(ref, {:error, "The ledger is closed."})

The cells show saving until the reply; then the grid reads its rows again, and an error refuses the changes, which show their old values. New rows and rows brought back need their ids at once: they cannot be saved later.

Summary

Functions

Gives the changes without a result one result, of fun of all of them: {:ok, rows}, the rows as saved in the order of the changes (or :ok for a delete), or {:error, reason}, which refuses every one (the whole batch, as a transaction does). {:error, change, reason} says which change it was: the others say that they were not saved.

The changes without a result are saved later, by a process of yours, which says how it went with Masu.reply(write.ref, result): :ok, {:ok, message} or {:error, message}. Not for :create and :restore, whose rows need their ids at once.

Gives each change without a result the result of fun: {:ok, row}, with the row as saved, or {:error, reason}. For :delete, :ok or {:ok, _} is the row deleted.

The result of one change: refused, for reason.

The changes refused, with their reasons.

The result of one change: saved, as row.

The changes without a result yet.

Records a change that your function saved, as row, in the grid's history (Masu.History) now: in the transaction your function runs it in, when the history is in the same repo. The grid does not record it again after your function. Without a history, it does nothing.

The result of a change, or nil.

Saves the changes without a result the grid's own way

The rows saved: as saved, in the order of their changes.

Types

op()

@type op() :: :update | :create | :delete | :restore

reason()

@type reason() ::
  Ecto.Changeset.t() | String.t() | [{String.t() | atom(), String.t()}]

result()

@type result() :: {:ok, term()} | {:error, reason()} | :pending | saved_stale()

t()

@type t() :: %Masu.Write{
  by: term(),
  changes: [Masu.Write.Change.t()],
  grid: String.t(),
  grid_config: map(),
  op: op(),
  origin: atom(),
  recorded: [non_neg_integer()],
  ref: String.t(),
  results: %{required(non_neg_integer()) => result()},
  scope: term()
}

Functions

all(write, fun)

@spec all(t(), ([Masu.Write.Change.t()] -> term())) :: t()

Gives the changes without a result one result, of fun of all of them: {:ok, rows}, the rows as saved in the order of the changes (or :ok for a delete), or {:error, reason}, which refuses every one (the whole batch, as a transaction does). {:error, change, reason} says which change it was: the others say that they were not saved.

async(write)

@spec async(t()) :: t()

The changes without a result are saved later, by a process of yours, which says how it went with Masu.reply(write.ref, result): :ok, {:ok, message} or {:error, message}. Not for :create and :restore, whose rows need their ids at once.

each(write, fun)

@spec each(t(), (Masu.Write.Change.t() -> term())) :: t()

Gives each change without a result the result of fun: {:ok, row}, with the row as saved, or {:error, reason}. For :delete, :ok or {:ok, _} is the row deleted.

error(write, change, reason)

@spec error(t(), Masu.Write.Change.t(), reason()) :: t()

The result of one change: refused, for reason.

failed(write)

@spec failed(t()) :: [{Masu.Write.Change.t(), reason()}]

The changes refused, with their reasons.

ok(write, change, row \\ nil)

@spec ok(t(), Masu.Write.Change.t(), term()) :: t()

The result of one change: saved, as row.

open(write)

@spec open(t()) :: [Masu.Write.Change.t()]

The changes without a result yet.

record(write, change, row)

@spec record(t(), Masu.Write.Change.t(), term()) :: t()

Records a change that your function saved, as row, in the grid's history (Masu.History) now: in the transaction your function runs it in, when the history is in the same repo. The grid does not record it again after your function. Without a history, it does nothing.

def update(write) do
  Masu.Write.each(write, fn change ->
    Repo.transaction(fn ->
      order = Orders.update!(change.row, change.params)
      Masu.Write.record(write, change, order)
      order
    end)
  end)
end

Gives the write, which knows the change is recorded.

result(write, change)

@spec result(t(), Masu.Write.Change.t()) :: result() | nil

The result of a change, or nil.

save(write)

@spec save(t()) :: t()

Saves the changes without a result the grid's own way:

  • :update - row |> changeset.(params), then Repo.update for an Ecto source, each row read, compared again and saved in a transaction that locks it; for a list, the changeset applied;
  • :create - changeset.(struct, params), inserted by the repo, or applied for a list; the grid's create_with fields go on the struct first, and on the changeset after, over what it cast;
  • :delete - Repo.delete, in a transaction that locks the row; a list's row taken out of its copy;
  • :restore - the row inserted again; a list's put back.

An update and a new row need the grid's changeset (and struct).

saved(write)

@spec saved(t()) :: [term()]

The rows saved: as saved, in the order of their changes.