MishkaCmsCore.Runtime.ExtensionProvision (Mishka CMS Core v0.0.3)

Copy Markdown View Source

Gives a site everything an extension ships and it is missing: the permission names, the components, the sample pages, and the snippet an installed editor offers. 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.

The snippet belongs to the editor

The core keeps no snippet of its own: a snippet is a row of whichever visual editor the host installed, offered back in that editor's list. So the run hands it over on runtime:provision:snippets, a transform event, with the payload in snippet_payload/0. The editor's plugin writes the row the site is missing and adds its name to installed; a site that already has it is left alone, as everything else here is.

A WRITE THAT FAILS IS REPORTED, NOT RAISED. A plugin that raises is skipped and the event answers the payload it was given, which reads as "nothing to install" — so the plugin puts the reason in error, and the run raises with it, as a refused write raised here before. With no editor installed nothing answers, and the report says snippets: [].

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.

What runtime:provision:snippets is handed, and answers with.

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

manifest()

@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 an editor's snippet list leaves it out. permissions is optional too: the names the extension's own areas are gated by, each with its description.

report()

@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()]
}

snippet_payload()

@type snippet_payload() :: %{
  app: atom(),
  snippet: %{name: String.t(), source: String.t()},
  site_id: String.t(),
  site_name: String.t(),
  actor: map() | nil,
  installed: [String.t()],
  error: term() | nil
}

What runtime:provision:snippets is handed, and answers with.

snippet.source is already the markup for this site (the manifest's function, applied to site_name). A plugin appends the name of each snippet it wrote to installed, or sets error to the reason a write was refused; it never raises. actor is the person running the provision, or nil when the system does (the seeds, the install wizard).

Functions

allowed?(manifest, actor)

@spec allowed?(manifest(), map() | nil) :: boolean()

Whether actor holds every name in required/1, so a provision it starts can finish.

bundle_version(manifest)

@spec bundle_version(manifest()) :: String.t() | nil

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.

component_names(manifest)

@spec component_names(manifest()) :: [String.t()]

The component names the extension's bundle ships, read off the bundle itself.

ensure!(manifest, site, answers \\ %{}, opts \\ [])

@spec ensure!(manifest(), map() | String.t(), map(), keyword()) :: report()

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.

ensure_permissions(arg1)

@spec ensure_permissions(manifest()) :: [String.t()]

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.

missing(manifest, actor)

@spec missing(manifest(), map() | nil) :: [String.t()]

The names in required/1 that actor does not hold — what a refusal says to ask for.

moved(pages, answers)

@spec moved([map()], map()) :: [map()]

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 /blog and wants ours somewhere else entirely. The segment goes after a leading :lang, which only the first segment can be: /:lang/blog moves 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).

page_paths(manifest, site, actor \\ nil)

@spec page_paths(manifest(), map() | String.t(), map() | nil) :: [String.t()]

The paths of the sample pages, in the order they are created.

pages(manifest, site, actor \\ nil)

@spec pages(manifest(), map() | String.t(), map() | nil) :: [map()]

The sample pages as this checkout ships them, as the attribute maps a page create takes.

required(manifest)

@spec required(manifest()) :: [String.t()]

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 an installed editor saves the snippet for its page editor, which this name opens;
  • admin:dashboard:runtime:site — the site is read, to name the pages after it.