Pages a site is GIVEN rather than written by hand — an extension provisioning itself, the dev seeds — and the one question every one of those callers has to ask before it writes a row: is this page already somebody's?
Why the question is not "is this path taken"
MishkaCmsCore.Runtime.Page is unique by more than its address. unique_name_per_site refuses a
second page called blog however different its path, so a site the dev seeds touched — each of
them has a page named "Blog" at /blog — refused the blog extension's first sample page at a
path nothing had taken. Left to Ash.create! that is a raise in the MIDDLE of a run: the
definitions after it are never written, whatever the caller does after the pages is never
reached, and every later run stops at the same row, so the site stays half-provisioned for good
and the admin who pressed the button is told only that it failed.
Every identity, including the one added after this was written
missing/3 reads the identities off the resource and asks about each of them, so a page is left
alone when ANY of them is held and the rest of the run happens regardless. Nothing here names
:name or :path: the day a third unique constraint is added to the resource, it is checked.
Two kinds of identity are not asked about:
- one keyed on a value the row only gets when it is written —
preview_tokenis minted inside the create action,language_group_idby a before-action change — because the caller cannot say what to look for; - one keyed on a
nilof ours that it counts as distinct, which cannot collide with anything.
create/2 is where the first kind is caught, and with it the row another run wrote in between:
a create refused by a unique index is read as the page being taken and reported as a skip, and so
is one refused because another page answers at its public address
(MishkaCmsCore.Runtime.Resources.Validations.PublicAddress) — /:lang/blog on a site whose
/blog is a page of its own. Every other error is raised, because a template that does not
compile is not a page somebody already has.
A held NAME is not a held address
The two are not the same answer and used to get the same one. An address is what the page IS — a
reader types it, a component links to it, and a site that answers there answers with whatever is
there already — so a held path is a real skip. A name is only how an admin finds the page in a
list, and every dev-seeded site here holds blog at /blog: the one page that carries the blog's
index was therefore skipped on every seeded site, at an address nothing had taken, and no later
run was ever going to write it.
So a definition whose ONLY held identities are keyed on :name is written under the next free
name — Blog 2, Blog 3 — and create/2 reports it under renamed, which is the other half:
a page that quietly appears under a name nobody asked for is as bad as one that never appears.
@name_attempts bounds the search, and a definition that runs out of names is skipped the way it
was before. Any other identity being held is still a skip, because nothing here can say what
would free it.
The two calls, and why they are two
Callers ask what is missing, then write it:
site_id
|> PageProvision.missing(definitions)
|> PageProvision.create(&write_page!/1)MishkaCmsCore.Dev.Seeds is why the plan is handed back rather than kept inside: it builds a
layout for its pages, and only when there is a page to hang on one — a layout made on every run
is the one thing a reseed used to leave growing behind it.
A definition is the attribute map the create will take, which is what makes the comparison
honest: the values looked for are the values that will be written, with the action's own
slugify changes applied the way the row will carry them. Attributes the caller leaves to the
create are the ones this cannot ask about.
Both calls report in the same shape. note/1 turns one skipped page — or one written under
another name — into the line a dashboard shows an admin, and answered?/1 says which kind of skip
it was: a page the site already answers at, or one nothing is ever going to write.
Summary
Types
The attributes a page create is given, as the caller will hand them to it.
A page left alone: the address it would have answered at, and the key-value pairs of every identity something already there holds. The attributes themselves are not carried — a page's template is most of it, and this is what a report is written from.
A page written under a name of its own: its address, the name its row carries, and the identity something else holds under the name it asked for.
A page nothing holds yet, with the values its row will carry. renamed is the identity something
else holds under the name the definition asked for, and nil where it got the name it asked for.
Functions
Whether the site answers at the address the skipped page would have had.
Writes each page of a missing/3 plan with write, and returns %{created: attrs, renamed: renamed, skipped: held} — the skipped ones being the plan's own plus any the database
refused, and the renamed ones those written under a name the definition did not ask for.
Splits definitions into the pages site_id can still be given and the ones something already
holds — the first group carrying the name each page will actually be written under.
One line saying what a run left alone and why, or what it had to call a page whose own name was taken, for a page that has to tell somebody — an admin reading a dashboard, a developer reading a seed run.
Whether source is what gave the site this page — false for a page nothing marked.
Which extension gave the site this page, as the string stamp/3 wrote, or nil for a page
nothing marked — one written by hand, or provisioned before pages were stamped.
The version stamp/3 recorded, or nil. See provisioned_by/1 about nil.
The language a page's routing row carries: none for a page that serves_all_languages?/1, else
its own. MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState reads nil as "answers whichever
language the URL captured".
Whether this page answers every language its site publishes, rather than one.
Marks attrs as a page that answers every language — see serves_all_languages?/1 — when its
:path captures lang, and clears the mark when it does not: only a lang capture can answer
more than one language, and a page refuses the mark on any other path.
The attributes with a mark saying which extension gave the site this page, and at what version.
Types
The attributes a page create is given, as the caller will hand them to it.
A page left alone: the address it would have answered at, and the key-value pairs of every identity something already there holds. The attributes themselves are not carried — a page's template is most of it, and this is what a report is written from.
A page written under a name of its own: its address, the name its row carries, and the identity something else holds under the name it asked for.
@type writable() :: %{ attrs: attrs(), values: %{optional(atom()) => term()}, renamed: keyword() | nil }
A page nothing holds yet, with the values its row will carry. renamed is the identity something
else holds under the name the definition asked for, and nil where it got the name it asked for.
Functions
Whether the site answers at the address the skipped page would have had.
THE TWO KINDS OF SKIP ARE NOT ONE THING, and every report made of them has to tell them apart: a page left alone because its own row is already there is the ordinary case of a run repeated, while one whose NAME a DIFFERENT page holds was never written and no later run will write it. Told that the second is "already there", whoever reads the report is told the opposite of what happened.
Writes each page of a missing/3 plan with write, and returns %{created: attrs, renamed: renamed, skipped: held} — the skipped ones being the plan's own plus any the database
refused, and the renamed ones those written under a name the definition did not ask for.
write is given one attribute map and is expected to raise on failure, which Ash.create! does.
Splits definitions into the pages site_id can still be given and the ones something already
holds — the first group carrying the name each page will actually be written under.
definitions are attribute maps for a page create; site_id is the site they are for, whether
or not they name it themselves. Pass action: :create when the write goes through the tenant
action rather than :master_create, so the attributes are compared as that action stores them.
Pass actor: user and the reads are made as that user, so the page resource's own policy answers
for them. Leave it out — the wizard, which runs before there is a user, and the seeds, which have
no session — and they are made as the system.
Pass stamp: {:mishka_blog, "3.0.1"} — or just stamp: :mishka_blog — and every page written
from this plan says which extension gave it to the site. See stamp/3, and provisioned_by/1
for reading it back. A provisioner that writes pages should always pass it; without it its pages
are indistinguishable from the admin's own.
One line saying what a run left alone and why, or what it had to call a page whose own name was taken, for a page that has to tell somebody — an admin reading a dashboard, a developer reading a seed run.
Whether source is what gave the site this page — false for a page nothing marked.
PageProvision.ours?(page, :mishka_blog)
Which extension gave the site this page, as the string stamp/3 wrote, or nil for a page
nothing marked — one written by hand, or provisioned before pages were stamped.
nil is not "not ours". It is "nothing here says", and a caller deciding whether it may touch a
row has to treat the two differently: an unmarked page at the address we wanted is the admin's
until they say otherwise.
The version stamp/3 recorded, or nil. See provisioned_by/1 about nil.
The language a page's routing row carries: none for a page that serves_all_languages?/1, else
its own. MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState reads nil as "answers whichever
language the URL captured".
The boot pass and the worker that runs after every save both ask this. Decided in each of them,
the two disagreed: /fa/blog answered after a restart and 404ed after the page was edited.
Whether this page answers every language its site publishes, rather than one.
True of a page built from components that read the captured language — the blog's index, a document's section — and false of one whose template holds prose, which answers the language it was written in and leaves the others to editions of their own.
The mark is read under the string key a row stores and under the atom a compiled page's extra
carries once MishkaCmsCore.Runtime.Helpers.atomize_keys/1 has converted it.
Marks attrs as a page that answers every language — see serves_all_languages?/1 — when its
:path captures lang, and clears the mark when it does not: only a lang capture can answer
more than one language, and a page refuses the mark on any other path.
The attributes with a mark saying which extension gave the site this page, and at what version.
source is the extension's OTP app — :mishka_blog — and version the bundle version it was
written against. Both go into extra, beside whatever the definition already put there.
PageProvision.stamp(attrs, :mishka_blog, "3.0.1")Pass it through missing/3's :stamp option rather than calling it per page; that way every
definition in a run is marked the same way and a caller cannot mark half of them.