Gives a site everything an extension ships and it is missing: the permission names, the
components, the sample pages, the snippet. One implementation, driven by a manifest/0 the
extension declares.
Why it is here and not in each extension
It was written twice. A diff of the blog's machinery against the document's reported the alias,
the section name, the snippet name, the path constants and the prose — and not one line of logic.
Two copies of a run that must never half-finish is two places for it to learn to.
What an extension keeps is what only it knows: the pages it ships and what they are made of. What it hands over is the order those things have to happen in, and every reason behind that order.
The order is the whole design
PERMISSION NAMES FIRST, so the extension's areas can be granted from the moment anything of it is
on a site, and a run that stops halfway still leaves them grantable. They are not the site's: a
name is shared by every site, so the first site's run creates it and every later one finds it.
The SYSTEM writes them, whoever runs the provision, because creating a permission name is a
master's action (MishkaCmsCore.Accounts.Permission), and a site admin setting their own site up
has to be able to finish.
COMPONENTS BEFORE PAGES. A page names live components, and
MishkaCmsCore.Runtime.Resources.Validations.StatefulIds reads their rows to decide whether two
placements collide. It can only do that once the rows are there.
NOTHING IS EVER UPDATED. Every step writes only what the site has no row for, so a second run adds
whatever the extension has learned to ship since and leaves everything else — including anything
the admin has edited — exactly as it is. An admin pressing a button called "set up" has not asked
to lose an edit; MishkaCmsCore.Runtime.Handlers.UiKitUpgrade is where losing one is offered, by
name, one row at a time.
EVERY PAGE HANGS ON THE SITE'S FRAME. An extension's pages are drawn in the site's site-main
layout, between the header and the footer every page of the site shares
(MishkaCmsCore.Runtime.SiteLayout), which every site has from its first install
(MishkaCmsCore.Install.StarterContent). An extension writes that layout's id into each page it
ships (MishkaCmsCore.Runtime.SiteLayout.layout_id/1).
NO PAGE MAY RAISE THE RUN. A site is unique by more than a page's address, so a site already
holding a page called "Blog" refuses a sample page at a path nothing has taken — and left to
Ash.create! that is a raise in the MIDDLE of the run: the pages after it are never written, the
snippet is never reached, and every later run stops at the same row. MishkaCmsCore.Runtime.PageProvision
is what turns that into a rename or a skip, and a line in the report either way.
What the report is for
Not the logs. The admin pressed a button and is owed the answer: what arrived, what was written
under another name because the one it wanted was taken, and what was left alone because something
of theirs holds it. PageProvision.note/1 writes each of those as a sentence.
Summary
Types
What an extension declares about itself. pages and snippet are functions of the SITE because a
stored <.component> call names the site whose component it draws, so the markup cannot be
written until the site is known.
Functions
Whether actor holds every name in required/1, so a provision it starts can finish.
The bundle version the pages are stamped with.
The component names the extension's bundle ships, read off the bundle itself.
Gives site everything manifest ships and it is missing, and returns what happened.
Creates the names manifest lists under permissions: that are not stored yet, as the system, and
returns the ones it created. ensure!/4 runs it first.
The names in required/1 that actor does not hold — what a refusal says to ask for.
The pages of a run with the addresses the admin asked for.
The paths of the sample pages, in the order they are created.
The sample pages as this checkout ships them, as the attribute maps a page create takes.
The names a person needs to run a provision of manifest — Set up, Doctor, Kit upgrade — one per
step, each asked on its own because a required list is any-of
Types
@type manifest() :: %{ :app => atom(), :bundle_path => String.t(), :section => String.t(), :pages => (map() -> [map()]), optional(:snippet) => %{name: String.t(), source: (String.t() -> String.t())}, optional(:permissions) => [{String.t(), String.t()}] }
What an extension declares about itself. pages and snippet are functions of the SITE because a
stored <.component> call names the site whose component it draws, so the markup cannot be
written until the site is known.
snippet is optional — an extension with nothing to put in the Studio's list leaves it out.
permissions is optional too: the names the extension's own areas are gated by, each with its
description.
@type report() :: %{ version: String.t() | nil, permissions: [String.t()], components: [String.t()], pages: [String.t()], snippets: [String.t()], renamed_pages: [MishkaCmsCore.Runtime.PageProvision.renamed()], skipped_pages: [MishkaCmsCore.Runtime.PageProvision.held()] }
Functions
Whether actor holds every name in required/1, so a provision it starts can finish.
The bundle version the pages are stamped with.
It is the KIT's version rather than the extension's: the pages and the components are one release
together — a page drawing a component the site has not got renders an error where the block should
be — and the kit is what MishkaCmsCore.Runtime.Handlers.UiKitUpgrade already versions.
The component names the extension's bundle ships, read off the bundle itself.
Gives site everything manifest ships and it is missing, and returns what happened.
Takes the site record or its id. Raises when the site is not there, when the actor lacks a name in
required/1, or when a write is refused for any reason but a page being taken: every caller
wants to know, and the screen says so. With no actor — the seeds, the install wizard's own run —
the system writes.
Creates the names manifest lists under permissions: that are not stored yet, as the system, and
returns the ones it created. ensure!/4 runs it first.
The names in required/1 that actor does not hold — what a refusal says to ask for.
The pages of a run with the addresses the admin asked for.
A page whose PATH something else holds is the one thing a run cannot answer on its own: the
address is taken, and only the admin knows whether to move ours or theirs. answers is what they
said, either way:
%{"paths" => %{"/:lang/blog" => "/:lang/journal"}}moves one page;%{"prefix" => "/mish"}moves every page of the extension under one segment, which is the answer when a site has its own/blogand wants ours somewhere else entirely. The segment goes after a leading:lang, which only the first segment can be:/:lang/blogmoves to/:lang/mish/blog.
A prefix is applied first and a named path wins over it, so an admin can move the lot and then
place one of them by hand. A page moved to a path that captures no lang answers in its own
language alone (MishkaCmsCore.Runtime.PageProvision.serving_all_languages/1).
The paths of the sample pages, in the order they are created.
The sample pages as this checkout ships them, as the attribute maps a page create takes.
The names a person needs to run a provision of manifest — Set up, Doctor, Kit upgrade — one per
step, each asked on its own because a required list is any-of:
admin:dashboard:runtime:component— the kit's components are written;admin:dashboard:runtime:js_hook,admin:dashboard:runtime:stylesheet— when the bundle ships JS hooks or stylesheets, the kit writes them too;admin:dashboard:runtime:page— the sample pages are written, and the snippet is saved for the page builder, which this name opens;admin:dashboard:runtime:site— the site is read, to name the pages after it.