MishkaCmsMigration.Workers.Helpers.Decision (Mishka CMS Migration v0.0.1)

Copy Markdown View Source

What a dry run or an import does with each item of a page (call/3), against what earlier runs recorded for the site (MishkaCmsMigration.MigrationItem), found by the run's source host, the item's kind and its source id. An admin's choice on the item comes first, before anything the source or the CMS says:

  • the admin left it out (left_out) — {:skipped, "left_out_by_admin"}, whatever the source changed and whatever the CMS holds; a CMS record an import wrote for it is kept as it is;
  • the admin mapped it to a CMS record (mapped_id) — {:mapped, target, record}, the record, which the import connects it to and never writes, whatever the source changed, what an earlier run wrote for it and what the target's existing/2 would find; when that record is gone, {:skipped, "mapped_missing"}, and when an admin archived it, as below.

Then, what earlier runs recorded:

  • the CMS record it was written or mapped to is archived (see "An archived record") — it waits for the admin's decision, {:undecided, "archived_in_cms"}, whatever the source changed;
  • none, or one never written — {:created, target, nil, decisions}, unless the target finds it in the CMS already (MishkaCmsMigration.Target's existing/2): {:connected, target, record}, the CMS record it is, {:updated, target, record, decisions}, the CMS record it is written over, {:created, target, nil, decisions, prepared}, created with what the target found for its write, {:skipped, reason}, why it is left out, or {:undecided, conflict}, a conflict on the site no admin has decided yet;
  • the CMS record it was written to is gone, deleted for good (see "A deleted record") — it waits for the admin's decision, {:undecided, "deleted_in_cms"}, whatever the source changed;
  • its MishkaCmsMigration.Targets.Helpers.LanguageUrls.hash/2 is the one recorded — {:skipped, "unchanged"}: an item that writes links is changed, to be written again, when the site's language address mode changed since it was written;
  • it was connected to a record the CMS already held (no target_updated_at was recorded): the record is the CMS's own and is never written — {:skipped, "connected"};
  • the CMS record's updated_at is not the one recorded when it was written: an admin changed it in the CMS — {:skipped, "edited_in_cms"};
  • otherwise — {:updated, target, record, decisions}, the CMS record to write over.

An archived record

A resource with AshArchival.Resource hides a record an admin archived from its reads, so an item's record that is not read is looked for among the archived ones (MishkaCmsMigration.Workers.Helpers.Archived), in one more query for the page and only when one is not read. Writing over an archived record, or pointing another at it, such as a post's tag, is refused, so the item waits for the admin's decision on "archived_in_cms", in a dry run and in an import, and what points at it waits for that decision too (MishkaCmsMigration.Workers.Helpers.Needs). Leave it archived ("leave_out") leaves the item out, as "left_out_by_admin", and what points at it is written without it. Restore it ("import") is {:restored, target, record, decisions, then}: the import restores the record as the admin and goes on as it would have, had it never been archived (then):

  • :written — the source changed it and no admin did, so it is written over;
  • :unchanged — the source did not change it, so it is kept as it is, and recorded as the import's own again;
  • :kept — an admin changed it before archiving it, or it is a record the CMS already held or the admin mapped the item to, so it is kept as the admin left it.

A deleted record

An item's record that is read neither among the live ones nor among the archived ones was deleted for good, so it is known in the same reads, with no query of its own. There is nothing to write over, and what points at it would lose it, so the item waits for the admin's decision on "deleted_in_cms", in a dry run and in an import, and what points at it waits for that decision too, as for an archived one. Import it again ("import") is {:created, target, nil, decisions}: the item is planned as a first import plans it, the target's existing/2 asked about it with the page's other new items, and the import records the new record in place of the deleted one and forgets the decision (MishkaCmsMigration.Workers.Helpers.Writer), so it holds once: a later deletion of the new record asks again. Leave it out ("keep") is {:skipped, "target_missing"} on every later run, and what points at it is written without it. A record an admin mapped the item to that is gone is "mapped_missing" instead, above: the import never wrote it.

decisions are the admin's answers to the conflicts the item met (MishkaCmsMigration.MigrationItem's decisions), which the target is handed with the item (MishkaCmsMigration.Target.decision/3); existing/2 is handed those of the page's items it is asked about.

The CMS records are read through the target resource's primary read as the admin running the job, under the CMS's own policies. When the CMS does not let the admin read them, each item that needs its record (one an earlier run wrote, or an admin mapped) fails as {:failed, "forbidden: <kind>"}, "You may not change <kinds> on this site", and the walk goes on; any other error of the read fails the attempt, as an error of the walk does (MishkaCmsMigration.Workers.Helpers.Pipeline).

An item whose kind has no target (MishkaCmsMigration.Target) is {:skipped, "no_target"}, unless it is a kind the Blog writes (MishkaCmsMigration.Application.blog_kinds/0) on a node without the Blog: then it waits for an admin's decision, {:undecided, "blog_missing"}, until the admin left it out ({:skipped, "left_out_by_admin"}) or decided to wait for the Blog ({:skipped, "waits_for_blog"}), and a later run, once the Blog is there, plans it as any other. One the run's choices (MishkaCmsMigration.MigrationRun's options) leave out wherever the Blog is, as its target does, is {:skipped, "not_chosen"} instead, with nothing to decide: a comment while comments is off (MishkaCmsMigration.Targets.BlogComment), and a post's old address while post_address is "blog_without_redirects" (MishkaCmsMigration.Targets.PostRedirect).

An item no run has met that the run's choices leave out (MishkaCmsMigration.MigrationRun.left_out?/3: a kind switched off in the New import window, or a site setting such as the icon) is planned as the admin's own leave-out, {:skipped, "left_out_by_admin"}, and recorded as left out (MishkaCmsMigration.Workers.Helpers.Writer), so the Items page lists it as Left out by you and Bring back undoes it. An item a run has met already keeps what the admin did with it.

Items are found by source host as well as by kind and id, so several sources moved into one site never meet: the same id from two hosts is two items, each with its own CMS record.

A page is read twice, whatever its size: once for the items earlier runs recorded, once for the CMS records they were written to, and a third time when one of those records is not read, for the archived ones, which tells the deleted ones too; and its items no run has written, or an admin decided to import again, are handed to the target's existing/2 together, once.

Summary

Types

An admin's answers to the conflicts an item met, by conflict.

What to do with an item: write a new record, with what the target found for its write when it found anything, write over record, restore record an admin archived and go on as then says (see "An archived record"), record it against record the CMS already held, leave it, and why, or wait for an admin's decision on a conflict.

Functions

The plan for each of items, a page of kind, by source id.

The conflicts on the CMS record an item was written to, "archived_in_cms" and "deleted_in_cms": what points at the record waits for the admin's decision on them (MishkaCmsMigration.Workers.Helpers.Needs), and an item that meets one under a parent an admin left out waits for it too (MishkaCmsMigration.Workers.Helpers.Pipeline).

Types

decisions()

@type decisions() :: %{required(String.t()) => String.t()}

An admin's answers to the conflicts an item met, by conflict.

plan()

@type plan() ::
  {:created, module(), nil, decisions()}
  | {:created, module(), nil, decisions(), term()}
  | {:updated, module(), struct(), decisions()}
  | {:restored, module(), struct(), decisions(), :written | :unchanged | :kept}
  | {:connected, module(), struct()}
  | {:mapped, module(), struct()}
  | {:skipped, String.t()}
  | {:undecided, String.t()}

What to do with an item: write a new record, with what the target found for its write when it found anything, write over record, restore record an admin archived and go on as then says (see "An archived record"), record it against record the CMS already held, leave it, and why, or wait for an admin's decision on a conflict.

Functions

call(state, kind, items)

@spec call(map(), String.t(), [MishkaCmsMigration.Source.Item.t()]) :: %{
  required(String.t()) => plan()
}

The plan for each of items, a page of kind, by source id.

record_conflicts()

@spec record_conflicts() :: [String.t()]

The conflicts on the CMS record an item was written to, "archived_in_cms" and "deleted_in_cms": what points at the record waits for the admin's decision on them (MishkaCmsMigration.Workers.Helpers.Needs), and an item that meets one under a parent an admin left out waits for it too (MishkaCmsMigration.Workers.Helpers.Pipeline).