# `MishkaCmsCore.Runtime.PageProvision`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_provision.ex#L5)

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_token` is minted inside
    the create action, `language_group_id` by a before-action change — because the caller cannot
    say what to look for;
  * one keyed on a `nil` of 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.

# `attrs`

```elixir
@type attrs() :: %{optional(atom()) =&gt; term()}
```

The attributes a page create is given, as the caller will hand them to it.

# `held`

```elixir
@type held() :: %{path: String.t(), fields: keyword(), ours?: boolean()}
```

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.

# `plan`

```elixir
@type plan() :: {[writable()], [held()]}
```

# `renamed`

```elixir
@type renamed() :: %{path: String.t(), name: String.t(), fields: keyword()}
```

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.

# `report`

```elixir
@type report() :: %{created: [attrs()], renamed: [renamed()], skipped: [held()]}
```

# `writable`

```elixir
@type writable() :: %{
  attrs: attrs(),
  values: %{optional(atom()) =&gt; 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.

# `answered?`

```elixir
@spec answered?(held()) :: boolean()
```

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.

# `create`

```elixir
@spec create(plan(), (attrs() -&gt; any())) :: report()
```

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.

# `missing`

```elixir
@spec missing(String.t(), [attrs()], keyword()) :: plan()
```

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.

# `note`

```elixir
@spec note(held() | renamed()) :: String.t()
```

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.

# `ours?`

```elixir
@spec ours?(map(), atom() | String.t()) :: boolean()
```

Whether `source` is what gave the site this page — `false` for a page nothing marked.

    PageProvision.ours?(page, :mishka_blog)

# `provisioned_by`

```elixir
@spec provisioned_by(map()) :: String.t() | nil
```

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.

# `provisioned_version`

```elixir
@spec provisioned_version(map()) :: String.t() | nil
```

The version `stamp/3` recorded, or `nil`. See `provisioned_by/1` about `nil`.

# `routing_language`

```elixir
@spec routing_language(map()) :: String.t() | 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.

# `serves_all_languages?`

```elixir
@spec serves_all_languages?(map()) :: boolean()
```

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.

# `serving_all_languages`

```elixir
@spec serving_all_languages(map()) :: map()
```

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.

# `stamp`

```elixir
@spec stamp(attrs(), atom() | String.t(), String.t() | nil) :: attrs()
```

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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
