What the migration remembers about one item of a source on a site: what the last dry run would
do with it (plan, plan_reason), the CMS record the last import wrote (target_resource,
target_id), when that record last changed as the import left it (target_updated_at), what the
source said about the item then (title, source_slug, source_modified_at, content_hash, and
the fields the CMS has no home for, extra), the SHA-256 of the file an import downloaded for a
media item (file_hash, which finds a second attachment of the same bytes), and what the last import that met it did
(outcome, reason, run_id). outcome is nil for an item no import has met yet, so a run's
counts (MishkaCmsMigration.MigrationRun's created_count and the rest) count what its import
did alone. The item's content and its other fields, such as a post's status and author, are not
kept: every run reads them from the source again.
An item is one per site, source host, kind and source id (:unique_source_item). That is what
makes running again safe: a second run finds the record the first one wrote and updates it, leaves
it when the source has not changed it, and never overwrites one an admin changed in the CMS since
(MishkaCmsMigration.Workers.Helpers.Decision).
The source host is part of it so several sources can move into one site: two hosts' items never meet, the same id on both is two items, and re-running one host touches only its own. When two sources ask the CMS for the same thing (the same address, say), the target's action refuses the second and that item is recorded as failed with the CMS's reason; the first is never overwritten.
Written by the jobs alone, which AshOban's context lets through where every actor is refused
(AshOban.Checks.AshObanInteraction), as upserts on that identity: a dry run's :record_plan
for every item it meets, which sets its title and plan and nothing an import recorded, and, once a
dry run of a whole source has walked it all, :forget_plan on each item it did not meet, so an
item the source no longer lists has no plan; an import's :record after its CMS record was written, and
:record_outcome after an item was skipped or failed, which keeps what the last write recorded.
No import write touches the plan,
so an item shows what the last dry run would do beside what the last import did, and none of them
touches an admin's choices (below) but one: once :record gives an item a CMS record again, an
admin's decision on the one deleted for good ("deleted_in_cms") is forgotten
(:forget_deleted_decision, right after it), so Import it again holds once and a later
deletion of the new record asks again. Read by whoever may read the site's runs
(MishkaCmsMigration.MigrationRun).
An admin's choices
Before an import, an admin decides what it brings, item by item. :leave_out sets left_out:
every later dry run and import skips the item as "left_out_by_admin", whatever the source
changed, and leaves the CMS record an earlier import wrote for it as it is, never deleting it.
:bring_back clears it, so the next run plans the item again. Each makes that one change.
:choose with a mapped_id sets it to a record of the CMS resource the item's kind is written
to, on its site (MishkaCmsMigration.Resources.Validations.MappedRecord), and brings the item
back: every later run connects the item to that record and never writes it, whatever the source
changed, what an earlier import wrote and what the target would find by its slug; what sits
under it in the source goes under that record. Two of the source's terms are merged by mapping both to one
record, which the admin makes first. A record deleted since leaves the item skipped as
"mapped_missing". nil imports the item as its own again, and an item an import connected
through its mapping forgets that record (unmapped/2). A mapping made after an import does not
link again the posts that import wrote and the source has not changed since.
An item the import cannot place safely on its own, such as a setting the site already has a
value of its own for, waits for a decision: a dry run plans it, and an import records it, as
:undecided, with the conflict as the reason (MishkaCmsMigration.Report), and writes nothing
for it. :choose with a decision answers that conflict (conflict/1) with one of the choices
it offers (choices/1): "keep", the site stays as it is (its own is kept, the item is
connected to the record the site has, or it waits); "import", the item is written the import's
way; or "leave_out", where the conflict offers it. The answer is kept in decisions, by
conflict, brings the item back, and holds for every later dry run and import, whose target reads
it (MishkaCmsMigration.Target.decision/3). Once an import has written the item by it, or
connected it to the site's record, the decision took effect (settled?/1) and stays: :choose
refuses another, in words, and the item keeps the record it has. :decide_keep and
:decide_import give the same answer for the rows an admin picks, and leave the rows whose
conflict does not offer it, or whose decision took effect, as they are.
An item that waits for its parent, which has no CMS record (waits_for_parent/1), has one more
open conflict, "parent_missing" (conflicts/1): Import anyway ("import") has every later
import write it without its parent while the parent has none, at the top level, and Wait for its
parent ("keep") takes that back. :choose answers the conflict its conflict argument names.
:decide answers one conflict with one choice on every item of a query that waits for that
decision (waits_for_decision, of that waiting_conflict), in one atomic update, and leaves the
others as they are: what the Items page's Decisions panel runs for a whole group of what waits
(:waiting_for_decision, read grouped by waiting_groups/3, decided by decide_waiting/5). It
checks once, not per item, that no run of the site is running.
The master actions (:master_leave_out, :master_bring_back, :master_choose,
:master_decide_keep, :master_decide_import) do the same on any site.
A left-out item takes what hangs on it along (MishkaCmsMigration.Workers.Helpers.Pipeline): the
items under it in the source, such as a category's categories, a page's pages and a comment's
replies, are skipped as "parent_left_out"; a post is written without a left-out category, tag
or author; a left-out post's redirect and comments are skipped as "post_missing"; and the links
other posts have to it keep their old address, found by its source_slug
(:left_out_by_slugs).
A choice takes the same permission and the same site as reading the items, and is refused while
one of the site's runs is dry running or importing
(MishkaCmsMigration.Resources.Validations.SiteIdle). Each one is broadcast on
"migration_item:<site_id>:updated", for the Items table to draw the item again. What a job of
picked items records is broadcast there too (picked?/1), so the rows it settled are drawn again;
what a whole run's job records is never broadcast.
In the admin
The Items page (MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Index), reached from a run's
row, lists as a Gervaz table every item of the run's source host on the run's site, whichever
run met it last; its "Imported by this run" filter narrows them to the ones this run's import met
last. Each row shows the item's kind (MishkaCmsMigration.MigrationRun.kind_label/1), its title
with its source id under it, its Plan (plan_cell/1): what the last dry run would do with it,
or what it planned once an import met the item, as a pill with its reason in words (MishkaCmsMigration.MigrationRun.reason_label/2), its Last
import (last_import_cell/1): what the last import did with it, the same way, or Not imported,
links to its CMS record (record_paths, below) and when a run or a choice last changed it. It
filters by kind, plan and last import, searches the title and the source id, and keeps them and
the view it shows in the address under items. A row expands to what is kept of the item, as
JSON.
A row's Dry run this item and Import this item (Gervaz's :event row actions, Import once the
admin confirms), on an item not left out of a run that still holds what its source connects with
(MishkaCmsMigration.MigrationRun.connectable?/1), run the run's own job for that item alone, and Import
the picked items (Gervaz's :event bulk action) for the rows picked with the checkboxes
(MishkaCmsMigration.MigrationRun's "Picked items"); the page runs them
(MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Index).
A row's Leave out or Bring back makes that choice (Gervaz's :update row actions), and so do the
bulk actions of the same names (Gervaz's :update bulk actions) for the rows picked with the
checkboxes, Leave out once the admin confirms; picking every row picks what the table's filters
find, of the run's site and source host alone. A flash says how many they changed
(chosen/2). A category's or a tag's Map to… (Gervaz's :edit row action, which shows the
page's modal first) opens this resource's Gervaz form on the item, one mapped_id field whose
choices are the site's records of that kind in the language it leads with, and Import as its own
(map_options/1). An item with a conflict has Decide… instead while the conflict is open
(conflict/1), which opens the same form with
one decision field, the conflict's choices in words (decision_options/1), under a notice that
says why the item waits, in its Plan cell's words (decision_reason/1); the Plan filter's
Needs your decision lists them all, and Keep the site as it is and Import anyway decide the
picked rows together (for two attachments of the same file, "same_file", Keep both and Use the
original). The Plan cell shows the choice before the plan, and the record a mapped
item is connected to (mapped_paths). The table's other view, Map, draws the items as the source
nests them (MishkaCmsMigrationWeb.Live.Admin.Dashboard.Item.Tree), from the rows it keeps
loaded.
ancestors is a calculation: the items above the item at the source, the top one first, read
for a page of items in one query (MishkaCmsMigration.Resources.Calculations.Ancestors). The
Plan and Last import cells of an item that waits for its parent show them, each a button that
opens the row, whose Where it sits draws the branch with what can be done on each item; the Map
view places an item under parents the table has not loaded.
record_paths is a calculation: where the item's CMS record is in the admin and on the site
(MishkaCmsMigration.MigrationRun.record_paths/2), with its name, nil for an item with no
record or whose record is gone; mapped_paths is the same for the record an admin mapped it to.
Each reads the records of a page together, as the reader.
Summary
Functions
Apply the full GuardedStruct pipeline (sanitize → validate → derive →
main_validator) to attrs and return either {:ok, transformed_attrs}
or {:error, errors}. Wire this into an Ash.Resource.Change to plug
guardedstruct rules into Ash's changeset pipeline.
O(1) lookup of a field's compile-time metadata by name. Returns
nil for unknown fields.
Compile-time-baked MapSet of every field name owned by the
guardedstruct block. Used by GuardedStruct.AshResource.Change
to decide, in O(1), whether a key in changeset.atomics
belongs to the pipeline.
The choice an item's earlier decisions carry to conflict, one a later run found on it that no
admin has answered yet (MishkaCmsMigration.Target.decision/3): the choice the admin made on
the item's other conflicts, when they all agree, and conflict offers it (choices/1). Only a
conflict whose answer touches the item alone takes one: how its links are written
("language_urls"), the fallback for its widgets, a player it embeds and its component's name.
A conflict whose answer changes something else, such as the site's own header, setting or
language, a record an admin archived or deleted in the CMS, other pages or the item's place, is
asked again, and so is a slug, whose answer a target reads also where no conflict was found, and
any conflict while the item's decisions disagree. nil then.
The choices of conflict in words, the recommended one first, as {words, choice} pairs: what
the Decisions panel and Where it sits offer. A conflict whose choices name what the item is about,
such as Add French to the site, says them for any item (Add its language to the site).
What an admin may answer conflict with: "keep" and "import", "keep" and "leave_out",
"import" and "leave_out", or, for scripts, "import" (remove them) and "trust" (keep
them), which no bulk choice makes; none for nil or a conflict this extension does not know. A
record deleted for good in the CMS ("deleted_in_cms") offers Import it again ("import") and
Leave it out ("keep"): the site stays as it is, and the item is skipped as deleted on every
later run, not left out by the admin, so it is not asked again.
What the Items table says after its bulk Leave out, Bring back, Keep the site as it is or Import
anyway changed the rows summary names (Gervaz's
MishkaGervaz.Table.Web.Events.BulkActionResult): how many items it left out, brought back or
decided, in a flash, and for a decision how many it left as they were, having no conflict that
choice answers.
What the Items table says after a row's Leave out or Bring back changed item: Left out: or
Brought back: and the item's title, in a flash, as the bulk ones say how many (chosen/2).
The conflict item waits on for an admin's decision, such as "site_has_own", while it is open:
the reason of its last dry run's plan when that plan is :undecided, else of its last import
when that left it :undecided after a plan to write it, or with no plan, else the conflict an
admin decided (the first, for an item that met several) until the decision took effect
(settled?/1), so a decision can be changed until an import wrote or connected the item by it.
A menu no import has written yet always has one, "menu_goes_to", so the main menu, which goes
to the header unless an admin says otherwise, can be moved: Decide… marks its current place.
A reason that names what the conflict is about after ": ", as "draft_pages: Team, Leadership"
does, is the conflict before it. nil for an item that never met one, and for one whose decision
took effect: Decide… is shown only while it is not nil, and Map to… comes back.
Every conflict an admin may answer on item now: the one it waits on (conflict/1), and
"parent_missing" while it waits for its parent (waits_for_parent/1), which Import anyway
answers with "import": the import writes it without its parent while the parent has no CMS
record, and Wait for its parent ("keep") undoes that, also once an import wrote the item so
(without_parent?/1). :choose answers the one its conflict names, the first when it names
none.
The choice the Decide… form opens with for item: the one its options mark as current
(decision_options/1), nil for an item no admin decided yet.
Answers conflict with choice on every item of run's source host that waits for that
decision (:waiting_for_decision), as user, through :decide as one atomic update
(Ash.bulk_update/4, each item as :choose would answer it): those of opts[:kinds] alone
when given, of the ids opts[:ids] alone when given, and whose title holds opts[:search] or
whose source id it is when given. Answers how many it decided.
The Decide… form's choices (decision's options): the choices of the conflict the item the
form has loaded waits on (conflict/1), in words, such as Keep the site's own and Use the
WordPress one. None before the item is loaded, and for an item with no conflict, whose form
shows Map to… instead.
Why the item the Decide… form has loaded waits for a decision, shown above its choices: the
reason its Plan cell gives, in the same words (MishkaCmsMigration.MigrationRun.reason_label/2),
such as "This tag was deleted in Mishka CMS after it was imported", or the conflict an admin
decided while the decision has not taken effect. nil before the item is loaded and for an item
with no conflict, whose form shows Map to… instead.
The item id as user may read it: a master's on any site, a site admin's on their own site
alone, with what load names loaded, such as :ancestors. {:ok, nil} for an item that is not
there or that user may not read.
Validates that the keys in the provided input are valid for at least one action on the resource.
Same as input/1, except restricts the keys to values accepted by the action provided.
The Items table's Last import cell for item: what the last import did with it, as a pill
(data-role="item-outcome", its data-outcome the outcome), the reason in words under it
(data-role="item-reason"), or Not imported (an empty data-outcome) for an item no import has
met.
The Map to… form's choices (mapped_id's options): Import as its own, then the records of the
item's kind's CMS resource (MishkaCmsMigration.Target) on its site, in the language the site
leads with (MishkaCmsCore.Runtime.Languages.primary/1), by name, read as the admin. The kind
and the site are the item's, the record the form has loaded to update (state.form.data); a
form with no item loaded yet, a kind with no Map to… (mappable?/1) or one no target writes has
Import as its own alone.
Whether item has a Map to…: a category or a tag, the kinds whose CMS records an admin may
pick from (map_options/1).
Whether a notification of this resource is of an item a job of picked items recorded
(MishkaCmsMigration.Workers.Helpers.Pipeline), whose actions' shared context says
picked_items?: the pub_sub's filter on the jobs' creates, so only those are broadcast.
The Items table's Plan cell for item: what the last dry run would do with it, such as Would
create, or, once an import met the item, what it planned, such as Planned: create
(planned_word/1), as a pill (data-role="item-plan", its data-plan the plan, or connected for an item the dry run
connected to a record the CMS already held), the reason in words under it
(data-role="item-plan-reason", MishkaCmsMigration.MigrationRun.reason_label/2, which names
the item's kind where the reason is about the item itself). A dash before
the item's first dry run. An admin's choice is shown before the plan: Left out by you
(left_out) for an item left out, Brought back for one brought back since the last dry run
left it out, and Decided (data-plan="decided"), with the choice in words, for an item whose
conflict an admin decided since the last run that met it. An item that waits for a decision reads
Needs your decision, with the conflict in words, also when only its last import met it.
Every plan as a {words, plan} pair, in the order the report counts outcomes, then Decided: the
items whose waiting decision an admin answered (decided_waiting), which the Plan cell reads as
Decided although the stored plan is still :undecided.
What a plan says in words, as the Plan cell and the Plan filter say it, such as Would create for
:created.
The Plan filter: query narrowed to the items whose Plan cell reads as value says
(plan_cell/1). "undecided" keeps the items that wait for a decision no admin has made
(waits_for_decision), also those only their last import left waiting, "decided" those whose
waiting decision an admin answered (decided_waiting), and any other plan the items of that plan
that wait for nothing. No value leaves query as it is.
What a plan says in words once an import has met the item, as history, such as Planned: create
for :created: the Plan cell then shows what the last dry run planned, and the Last import cell
what was done.
What the Items table says when a row's Leave out, Bring back, Keep the site as it is or Import
anyway (Gervaz's on_row_action_error) is refused only because a dry run or an import of the
site is running (MishkaCmsMigration.MigrationRun.refused_while_running?/1): an info notice,
"Not done. …" or "Not decided. …", in place of Gervaz's "… failed", answered {:halt, socket} so
Gervaz puts none. Any other error is left to Gervaz.
refused/3 for the bulk actions (on_bulk_action_error): every error of summary must be that
refusal for the notice to replace Gervaz's flash.
The Items table's search: query narrowed to the items whose title holds search, in any case,
or whose source id holds it. A leading # is dropped, as the rows show a source id as #157.
No search leaves query as it is.
Whether the admin's decision on item took effect: an import wrote the item by it, or connected
it to the site's record, so it has a CMS record (target_id), and nothing on it waits for a
decision now. Changing it then could only move a record written as new onto the site's own
address, or leave one connected unwritten, so it stays
(MishkaCmsMigration.Resources.Changes.Decided).
Where item stands, as Where it sits shows each item of a branch: :not_listed for one the
source does not list (an ancestor with no id, MishkaCmsMigration.Resources.Calculations.Ancestors),
:left_out, :connected (mapped by an admin), :imported (it has a CMS record), :undecided
(it waits for a decision no admin has made, undecided?/1), :decided (an admin decided on it,
and it is ready to import, also when its last import failed, since the next import follows the
decision; Where it sits says under it why that import failed), :failed (its last import failed,
and nothing on it is decided) or :not_imported.
What standing/1 says, in words, such as Needs your decision.
Whether item waits for a decision no admin has made yet, as its Plan cell's Needs your decision
says (plan_cell/1): it is not left out or mapped, and the conflict it waits on is not one an
admin decided. What sits under it waits for that decision too
(MishkaCmsMigration.Workers.Helpers.Needs.parent/2).
What :choose does to the record an item was connected to when its mapping is taken away: an
item an import connected (it recorded no target_updated_at) forgets that record, so the next
run creates it, or meets it by its slug, as an item never written. An item an import wrote keeps
its own record.
What waits for a decision no admin has made among the items of run's source host on its site
(:waiting_for_decision, undecided?/1), read as user: %{kind: kind, conflict: conflict, count: count} for each kind and conflict, the largest first. search narrows them to the items
whose title holds it, whatever its case, or whose source id it is, with or without a leading #. One query, whatever the number
of items: each kind and conflict is one row, with its count.
Why item waits for its parent, which has no CMS record, such as
"parent_undecided: Team (#25)"; nil for an item that has a CMS record, that an admin left out
or mapped, and for one nothing holds for its parent.
Whether an admin decided that the item is imported without its parent while the parent has no
CMS record (Import anyway, "parent_missing" answered with "import"), given its decisions.
Types
@type t() :: %MishkaCmsMigration.MigrationItem{ __lateral_join_source__: term(), __meta__: term(), __metadata__: term(), __order__: term(), aggregates: term(), ancestors: term(), calculations: term(), content_hash: term(), decided_waiting: term(), decisions: term(), extra: term(), file_hash: term(), id: term(), inserted_at: term(), left_out: term(), mapped_id: term(), mapped_paths: term(), outcome: term(), plan: term(), plan_reason: term(), reason: term(), record_paths: term(), run: term(), run_id: term(), site: term(), site_id: term(), source_host: term(), source_id: term(), source_kind: term(), source_modified_at: term(), source_parent: term(), source_slug: term(), target_id: term(), target_resource: term(), target_updated_at: term(), title: term(), updated_at: term(), waiting_conflict: term(), waits_for_decision: term() }
Functions
Apply the full GuardedStruct pipeline (sanitize → validate → derive →
main_validator) to attrs and return either {:ok, transformed_attrs}
or {:error, errors}. Wire this into an Ash.Resource.Change to plug
guardedstruct rules into Ash's changeset pipeline.
The function is named __guarded_change__ because it does more than
validate — it can also transform values (trim, downcase, slugify,
auto-fill, etc.) before they reach the data layer.
O(1) lookup of a field's compile-time metadata by name. Returns
nil for unknown fields.
Compile-time-baked MapSet of every field name owned by the
guardedstruct block. Used by GuardedStruct.AshResource.Change
to decide, in O(1), whether a key in changeset.atomics
belongs to the pipeline.
The choice an item's earlier decisions carry to conflict, one a later run found on it that no
admin has answered yet (MishkaCmsMigration.Target.decision/3): the choice the admin made on
the item's other conflicts, when they all agree, and conflict offers it (choices/1). Only a
conflict whose answer touches the item alone takes one: how its links are written
("language_urls"), the fallback for its widgets, a player it embeds and its component's name.
A conflict whose answer changes something else, such as the site's own header, setting or
language, a record an admin archived or deleted in the CMS, other pages or the item's place, is
asked again, and so is a slug, whose answer a target reads also where no conflict was found, and
any conflict while the item's decisions disagree. nil then.
The choices of conflict in words, the recommended one first, as {words, choice} pairs: what
the Decisions panel and Where it sits offer. A conflict whose choices name what the item is about,
such as Add French to the site, says them for any item (Add its language to the site).
What an admin may answer conflict with: "keep" and "import", "keep" and "leave_out",
"import" and "leave_out", or, for scripts, "import" (remove them) and "trust" (keep
them), which no bulk choice makes; none for nil or a conflict this extension does not know. A
record deleted for good in the CMS ("deleted_in_cms") offers Import it again ("import") and
Leave it out ("keep"): the site stays as it is, and the item is skipped as deleted on every
later run, not left out by the admin, so it is not asked again.
@spec chosen( MishkaGervaz.Table.Web.Events.BulkActionResult.t(), Phoenix.LiveView.Socket.t() ) :: Phoenix.LiveView.Socket.t()
What the Items table says after its bulk Leave out, Bring back, Keep the site as it is or Import
anyway changed the rows summary names (Gervaz's
MishkaGervaz.Table.Web.Events.BulkActionResult): how many items it left out, brought back or
decided, in a flash, and for a decision how many it left as they were, having no conflict that
choice answers.
@spec chosen_row(map(), Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t()
What the Items table says after a row's Leave out or Bring back changed item: Left out: or
Brought back: and the item's title, in a flash, as the bulk ones say how many (chosen/2).
The conflict item waits on for an admin's decision, such as "site_has_own", while it is open:
the reason of its last dry run's plan when that plan is :undecided, else of its last import
when that left it :undecided after a plan to write it, or with no plan, else the conflict an
admin decided (the first, for an item that met several) until the decision took effect
(settled?/1), so a decision can be changed until an import wrote or connected the item by it.
A menu no import has written yet always has one, "menu_goes_to", so the main menu, which goes
to the header unless an admin says otherwise, can be moved: Decide… marks its current place.
A reason that names what the conflict is about after ": ", as "draft_pages: Team, Leadership"
does, is the conflict before it. nil for an item that never met one, and for one whose decision
took effect: Decide… is shown only while it is not nil, and Map to… comes back.
Every conflict an admin may answer on item now: the one it waits on (conflict/1), and
"parent_missing" while it waits for its parent (waits_for_parent/1), which Import anyway
answers with "import": the import writes it without its parent while the parent has no CMS
record, and Wait for its parent ("keep") undoes that, also once an import wrote the item so
(without_parent?/1). :choose answers the one its conflict names, the first when it names
none.
The choice the Decide… form opens with for item: the one its options mark as current
(decision_options/1), nil for an item no admin decided yet.
@spec decide_waiting(map(), map(), String.t(), String.t(), keyword()) :: {:ok, non_neg_integer()} | {:error, term()}
Answers conflict with choice on every item of run's source host that waits for that
decision (:waiting_for_decision), as user, through :decide as one atomic update
(Ash.bulk_update/4, each item as :choose would answer it): those of opts[:kinds] alone
when given, of the ids opts[:ids] alone when given, and whose title holds opts[:search] or
whose source id it is when given. Answers how many it decided.
A few queries whatever the number of items: the check that no run of the site is running, the
update, which answers the ids it changed, and one update of the source host's runs
(MishkaCmsMigration.MigrationRun.refresh_decisions/2), so their Result chips are drawn again. The items are not broadcast one by one: the caller
draws them again once, as the Items page does.
The Decide… form's choices (decision's options): the choices of the conflict the item the
form has loaded waits on (conflict/1), in words, such as Keep the site's own and Use the
WordPress one. None before the item is loaded, and for an item with no conflict, whose form
shows Map to… instead.
Why the item the Decide… form has loaded waits for a decision, shown above its choices: the
reason its Plan cell gives, in the same words (MishkaCmsMigration.MigrationRun.reason_label/2),
such as "This tag was deleted in Mishka CMS after it was imported", or the conflict an admin
decided while the decision has not taken effect. nil before the item is loaded and for an item
with no conflict, whose form shows Map to… instead.
The item id as user may read it: a master's on any site, a site admin's on their own site
alone, with what load names loaded, such as :ancestors. {:ok, nil} for an item that is not
there or that user may not read.
Validates that the keys in the provided input are valid for at least one action on the resource.
Raises a KeyError error at compile time if not. This exists because generally a struct should only ever
be created by Ash as a result of a successful action. You should not be creating records manually in code,
e.g %MyResource{value: 1, value: 2}. Generally that is fine, but often with embedded resources it is nice
to be able to validate the keys that are being provided, e.g
Resource
|> Ash.Changeset.for_create(:create, %{embedded: EmbeddedResource.input(foo: 1, bar: 2)})
|> Ash.create()
Same as input/1, except restricts the keys to values accepted by the action provided.
@spec last_import_cell(map()) :: Phoenix.LiveView.Rendered.t()
The Items table's Last import cell for item: what the last import did with it, as a pill
(data-role="item-outcome", its data-outcome the outcome), the reason in words under it
(data-role="item-reason"), or Not imported (an empty data-outcome) for an item no import has
met.
An import that waited for a decision reads Needs your decision while the item still waits for
it (undecided?/1). Once an admin answered that conflict, or nothing on the item waits any more,
it reads Waited for your decision, with what holds now in its Plan cell's words
(plan_cell/1), such as "Now decided: Header links: they show in the top bar" or "Now: Left
out by you". It reads nothing more than the row holds.
The Map to… form's choices (mapped_id's options): Import as its own, then the records of the
item's kind's CMS resource (MishkaCmsMigration.Target) on its site, in the language the site
leads with (MishkaCmsCore.Runtime.Languages.primary/1), by name, read as the admin. The kind
and the site are the item's, the record the form has loaded to update (state.form.data); a
form with no item loaded yet, a kind with no Map to… (mappable?/1) or one no target writes has
Import as its own alone.
Whether item has a Map to…: a category or a tag, the kinds whose CMS records an admin may
pick from (map_options/1).
@spec picked?(Ash.Notifier.Notification.t()) :: boolean()
Whether a notification of this resource is of an item a job of picked items recorded
(MishkaCmsMigration.Workers.Helpers.Pipeline), whose actions' shared context says
picked_items?: the pub_sub's filter on the jobs' creates, so only those are broadcast.
@spec plan_cell(map(), keyword()) :: Phoenix.LiveView.Rendered.t()
The Items table's Plan cell for item: what the last dry run would do with it, such as Would
create, or, once an import met the item, what it planned, such as Planned: create
(planned_word/1), as a pill (data-role="item-plan", its data-plan the plan, or connected for an item the dry run
connected to a record the CMS already held), the reason in words under it
(data-role="item-plan-reason", MishkaCmsMigration.MigrationRun.reason_label/2, which names
the item's kind where the reason is about the item itself). A dash before
the item's first dry run. An admin's choice is shown before the plan: Left out by you
(left_out) for an item left out, Brought back for one brought back since the last dry run
left it out, and Decided (data-plan="decided"), with the choice in words, for an item whose
conflict an admin decided since the last run that met it. An item that waits for a decision reads
Needs your decision, with the conflict in words, also when only its last import met it.
Every plan as a {words, plan} pair, in the order the report counts outcomes, then Decided: the
items whose waiting decision an admin answered (decided_waiting), which the Plan cell reads as
Decided although the stored plan is still :undecided.
What a plan says in words, as the Plan cell and the Plan filter say it, such as Would create for
:created.
@spec planned_as(Ash.Query.t(), term()) :: Ash.Query.t()
The Plan filter: query narrowed to the items whose Plan cell reads as value says
(plan_cell/1). "undecided" keeps the items that wait for a decision no admin has made
(waits_for_decision), also those only their last import left waiting, "decided" those whose
waiting decision an admin answered (decided_waiting), and any other plan the items of that plan
that wait for nothing. No value leaves query as it is.
What a plan says in words once an import has met the item, as history, such as Planned: create
for :created: the Plan cell then shows what the last dry run planned, and the Last import cell
what was done.
@spec refused(atom(), term(), Phoenix.LiveView.Socket.t()) :: Phoenix.LiveView.Socket.t() | {:halt, Phoenix.LiveView.Socket.t()}
What the Items table says when a row's Leave out, Bring back, Keep the site as it is or Import
anyway (Gervaz's on_row_action_error) is refused only because a dry run or an import of the
site is running (MishkaCmsMigration.MigrationRun.refused_while_running?/1): an info notice,
"Not done. …" or "Not decided. …", in place of Gervaz's "… failed", answered {:halt, socket} so
Gervaz puts none. Any other error is left to Gervaz.
@spec refused_bulk( MishkaGervaz.Table.Web.Events.BulkActionResult.t(), Phoenix.LiveView.Socket.t() ) :: Phoenix.LiveView.Socket.t() | {:halt, Phoenix.LiveView.Socket.t()}
refused/3 for the bulk actions (on_bulk_action_error): every error of summary must be that
refusal for the notice to replace Gervaz's flash.
@spec searched(Ash.Query.t(), String.t() | nil) :: Ash.Query.t()
The Items table's search: query narrowed to the items whose title holds search, in any case,
or whose source id holds it. A leading # is dropped, as the rows show a source id as #157.
No search leaves query as it is.
Whether the admin's decision on item took effect: an import wrote the item by it, or connected
it to the site's record, so it has a CMS record (target_id), and nothing on it waits for a
decision now. Changing it then could only move a record written as new onto the site's own
address, or leave one connected unwritten, so it stays
(MishkaCmsMigration.Resources.Changes.Decided).
Where item stands, as Where it sits shows each item of a branch: :not_listed for one the
source does not list (an ancestor with no id, MishkaCmsMigration.Resources.Calculations.Ancestors),
:left_out, :connected (mapped by an admin), :imported (it has a CMS record), :undecided
(it waits for a decision no admin has made, undecided?/1), :decided (an admin decided on it,
and it is ready to import, also when its last import failed, since the next import follows the
decision; Where it sits says under it why that import failed), :failed (its last import failed,
and nothing on it is decided) or :not_imported.
What standing/1 says, in words, such as Needs your decision.
Whether item waits for a decision no admin has made yet, as its Plan cell's Needs your decision
says (plan_cell/1): it is not left out or mapped, and the conflict it waits on is not one an
admin decided. What sits under it waits for that decision too
(MishkaCmsMigration.Workers.Helpers.Needs.parent/2).
@spec unmapped(Ash.Changeset.t(), map()) :: Ash.Changeset.t()
What :choose does to the record an item was connected to when its mapping is taken away: an
item an import connected (it recorded no target_updated_at) forgets that record, so the next
run creates it, or meets it by its slug, as an item never written. An item an import wrote keeps
its own record.
What waits for a decision no admin has made among the items of run's source host on its site
(:waiting_for_decision, undecided?/1), read as user: %{kind: kind, conflict: conflict, count: count} for each kind and conflict, the largest first. search narrows them to the items
whose title holds it, whatever its case, or whose source id it is, with or without a leading #. One query, whatever the number
of items: each kind and conflict is one row, with its count.
Why item waits for its parent, which has no CMS record, such as
"parent_undecided: Team (#25)"; nil for an item that has a CMS record, that an admin left out
or mapped, and for one nothing holds for its parent.
Whether it waits is what its last dry run or import said
(MishkaCmsMigration.Workers.Helpers.Needs.parent/2); what holds it is read from its parents as
they are now when its :ancestors are loaded, so it never goes stale once a parent is decided or
imported after the item was skipped: the top one of them that is not imported or connected
(standing/1), named as Needs.parent/2 names the parent, or as Needs.ancestor/2 names one
higher up, such as "ancestor_undecided: About (#24)". Once every one of them has a record, it
waits for nothing. Without its ancestors the stored reason is the answer.
Whether an admin decided that the item is imported without its parent while the parent has no
CMS record (Import anyway, "parent_missing" answered with "import"), given its decisions.