2019-03-05 11:09:15 -07:00
|
|
|
defmodule FarmbotCore.Asset.Private do
|
2019-03-19 12:04:24 -06:00
|
|
|
@moduledoc """
|
2019-03-20 10:59:21 -06:00
|
|
|
Private Assets are those that are internal to
|
|
|
|
Farmbot that _are not_ stored in the API, but
|
|
|
|
_are_ stored in Farmbot's database.
|
2019-03-19 12:04:24 -06:00
|
|
|
"""
|
2019-11-13 14:27:29 -07:00
|
|
|
require Logger
|
2020-05-17 11:08:37 -06:00
|
|
|
require FarmbotCore.Logger
|
2019-03-19 12:04:24 -06:00
|
|
|
|
|
|
|
alias FarmbotCore.{Asset.Repo,
|
|
|
|
Asset.Private.LocalMeta,
|
|
|
|
}
|
2018-10-29 10:33:52 -06:00
|
|
|
|
|
|
|
import Ecto.Query, warn: false
|
|
|
|
import Ecto.Changeset, warn: false
|
|
|
|
|
|
|
|
@doc "Lists `module` objects that still need to be POSTed to the API."
|
|
|
|
def list_local(module) do
|
2020-05-17 13:45:12 -06:00
|
|
|
Repo.all(from(data in module, where: is_nil(data.id)))
|
2018-10-29 10:33:52 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
@doc "Lists `module` objects that have a `local_meta` object"
|
|
|
|
def list_dirty(module) do
|
|
|
|
table = table(module)
|
|
|
|
q = from(lm in LocalMeta, where: lm.table == ^table, select: lm.asset_local_id)
|
2020-05-17 13:45:12 -06:00
|
|
|
Repo.all(from(data in module, join: lm in subquery(q)))
|
2020-05-16 16:33:48 -06:00
|
|
|
end
|
|
|
|
|
2018-10-29 10:33:52 -06:00
|
|
|
@doc "Mark a document as `dirty` by creating a `local_meta` object"
|
2019-04-24 17:31:54 -06:00
|
|
|
def mark_dirty!(asset, params \\ %{}) do
|
2018-10-29 10:33:52 -06:00
|
|
|
table = table(asset)
|
|
|
|
|
2020-05-17 13:45:12 -06:00
|
|
|
local_meta =
|
|
|
|
Repo.one(
|
|
|
|
from(lm in LocalMeta, where: lm.asset_local_id == ^asset.local_id and lm.table == ^table)
|
|
|
|
) || Ecto.build_assoc(asset, :local_meta)
|
2018-10-29 10:33:52 -06:00
|
|
|
|
2019-11-13 14:27:29 -07:00
|
|
|
## NOTE(Connor): 19/11/13
|
|
|
|
# the try/catch here seems unneeded here, but because of how sqlite/ecto works, it is 100% needed.
|
|
|
|
# Because sqlite can't test unique constraints before a transaction, if this function gets called for
|
|
|
|
# the same asset more than once asyncronously, the asset can be marked dirty twice at the same time
|
|
|
|
# causing the `unique constraint` error to happen in either `ecto` OR `sqlite`. I've
|
2020-05-14 13:31:46 -06:00
|
|
|
# caught both errors here as they are both essentially the same thing, and can be safely
|
2019-11-13 14:27:29 -07:00
|
|
|
# discarded. Doing an `insert_or_update/1` (without the bang) can still result in the sqlite
|
2020-05-14 13:31:46 -06:00
|
|
|
# error being thrown.
|
2019-11-13 14:27:29 -07:00
|
|
|
changeset = LocalMeta.changeset(local_meta, Map.merge(params, %{table: table, status: "dirty"}))
|
|
|
|
try do
|
2020-05-17 10:04:55 -06:00
|
|
|
Repo.insert_or_update!(changeset)
|
2019-11-13 14:27:29 -07:00
|
|
|
catch
|
|
|
|
:error, %Sqlite.DbConnection.Error{
|
2020-05-14 13:31:46 -06:00
|
|
|
message: "UNIQUE constraint failed: local_metas.table, local_metas.asset_local_id",
|
2019-11-13 14:27:29 -07:00
|
|
|
sqlite: %{code: :constraint}
|
2020-05-14 13:31:46 -06:00
|
|
|
} ->
|
2019-11-13 14:27:29 -07:00
|
|
|
Logger.warn """
|
|
|
|
Caught race condition marking data as dirty (sqlite)
|
|
|
|
table: #{inspect(table)}
|
|
|
|
id: #{inspect(asset.local_id)}
|
|
|
|
"""
|
|
|
|
Ecto.Changeset.apply_changes(changeset)
|
|
|
|
:error, %Ecto.InvalidChangesetError{
|
|
|
|
changeset: %{
|
2020-05-14 13:31:46 -06:00
|
|
|
action: :insert,
|
2019-11-13 14:27:29 -07:00
|
|
|
errors: [
|
|
|
|
table: {"LocalMeta already exists.", [
|
2020-05-14 13:31:46 -06:00
|
|
|
validation: :unsafe_unique,
|
2019-11-13 14:27:29 -07:00
|
|
|
fields: [:table, :asset_local_id]
|
|
|
|
]}
|
|
|
|
]}
|
|
|
|
} ->
|
|
|
|
Logger.warn """
|
|
|
|
Caught race condition marking data as dirty (ecto)
|
|
|
|
table: #{inspect(table)}
|
|
|
|
id: #{inspect(asset.local_id)}
|
|
|
|
"""
|
|
|
|
Ecto.Changeset.apply_changes(changeset)
|
2020-05-14 13:31:46 -06:00
|
|
|
type, reason ->
|
2019-11-13 14:27:29 -07:00
|
|
|
FarmbotCore.Logger.error 1, """
|
|
|
|
Caught unexpected error marking data as dirty
|
|
|
|
table: #{inspect(table)}
|
|
|
|
id: #{inspect(asset.local_id)}
|
|
|
|
error type: #{inspect(type)}
|
|
|
|
reason: #{inspect(reason)}
|
|
|
|
"""
|
|
|
|
Ecto.Changeset.apply_changes(changeset)
|
|
|
|
end
|
2018-10-29 10:33:52 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
@doc "Remove the `local_meta` record from an object."
|
|
|
|
@spec mark_clean!(map) :: nil | map()
|
|
|
|
def mark_clean!(data) do
|
|
|
|
Repo.preload(data, :local_meta)
|
|
|
|
|> Map.fetch!(:local_meta)
|
|
|
|
|> case do
|
|
|
|
nil -> nil
|
|
|
|
local_meta -> Repo.delete!(local_meta)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp table(%module{}), do: table(module)
|
|
|
|
defp table(module), do: module.__schema__(:source)
|
|
|
|
end
|