MishkaCmsMigration.MigrationItem (Mishka CMS Migration v0.0.1)

Copy Markdown View Source

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

t()

@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

__guarded_change__(attrs, error? \\ false)

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.

__guarded_field_meta__(name)

O(1) lookup of a field's compile-time metadata by name. Returns nil for unknown fields.

__guarded_field_name_set__()

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.

carried(decisions, conflict)

@spec carried(%{required(String.t()) => String.t()}, String.t()) :: String.t() | nil

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.

choice_options(conflict)

@spec choice_options(String.t()) :: [{String.t(), String.t()}]

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).

choices(conflict)

@spec choices(String.t() | nil) :: [String.t()]

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.

chosen(bulk_action_result, socket)

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.

chosen_row(item, socket)

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).

conflict(item)

@spec conflict(map()) :: String.t() | nil

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.

conflicts(item)

@spec conflicts(map()) :: [String.t()]

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.

current_decision(item)

@spec current_decision(map()) :: String.t() | nil

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.

decide_waiting(run, user, conflict, choice, opts \\ [])

@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.

decision_options(arg1)

@spec decision_options(map()) :: [{String.t(), String.t()}]

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.

decision_reason(arg1)

@spec decision_reason(map()) :: String.t() | nil

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.

default_short_name()

fetch(id, user, load \\ [])

@spec fetch(String.t(), map(), list()) :: {:ok, struct() | nil} | {:error, term()}

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.

input(opts)

@spec input(values :: map() | Keyword.t()) :: map() | no_return()

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()

input(opts, action)

@spec input(values :: map() | Keyword.t(), action :: atom()) :: map() | no_return()

Same as input/1, except restricts the keys to values accepted by the action provided.

last_import_cell(item)

@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.

map_options(arg1)

@spec map_options(map()) :: [{String.t(), String.t()}]

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.

mappable?(map)

@spec mappable?(map()) :: boolean()

Whether item has a Map to…: a category or a tag, the kinds whose CMS records an admin may pick from (map_options/1).

picked?(notification)

@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.

plan_cell(item, opts \\ [])

@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.

plan_options()

@spec plan_options() :: [{String.t(), atom()}]

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.

plan_word(atom)

@spec plan_word(atom()) :: String.t()

What a plan says in words, as the Plan cell and the Plan filter say it, such as Would create for :created.

planned_as(query, value)

@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.

planned_word(atom)

@spec planned_word(atom()) :: String.t()

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.

primary_key_matches?(left, right)

refused(action, reason, socket)

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_bulk(arg1, socket)

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.

searched(query, search)

@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.

settled?(item)

@spec settled?(map()) :: boolean()

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).

standing(item)

@spec standing(map()) :: atom()

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.

standing_word(atom)

@spec standing_word(atom()) :: String.t()

What standing/1 says, in words, such as Needs your decision.

undecided?(item)

@spec undecided?(map()) :: boolean()

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).

unmapped(changeset, context)

@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.

waiting_groups(run, user, search \\ nil)

@spec waiting_groups(map(), map(), String.t() | nil) ::
  {:ok, [map()]} | {:error, term()}

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.

waits_for_parent(item)

@spec waits_for_parent(map()) :: String.t() | nil

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.

without_parent?(arg1)

@spec without_parent?(map()) :: boolean()

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.