# `MishkaCmsCore.Install.Extensions`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/install/extensions.ex#L5)

Which extensions can install themselves onto a site, and the one call that asks one to.

## There is no registry here, because there already is one

An earlier draft of this invented a second event to ask "who is installable?". It was not needed:
`MishkaInstaller.Event.Event` IS that registry. A plugin subscribing to `installer:extension:install`
has already told the installer it exists, under which OTP app, at which priority, depending on
which other events, and whether an operator has switched it off — all persisted, all read back
with one call.

So SUBSCRIBING IS DECLARING. `installable/0` reads the records; nothing is kept in sync by hand,
and an extension that is not installed cannot appear in a list it never registered with.

What each record carries, and what we do NOT write ourselves:

| we need | the record's field |
| --- | --- |
| which extension | `:extension`, the OTP app |
| what to call it on screen | `:extra`, below |
| the order to offer them in | `:priority` |
| switched off everywhere | `:status` |
| one extension before another | `:depends`, with cycles already refused |

## `:extra` is the only shape we define

`Event.register/3` takes it from the plugin's own `initial:` and re-reads it on every upgrade, so
a label changed in a new release reaches installs that registered under the old one:

    use MishkaInstaller.Event.Hook,
      event: "installer:extension:install",
      initial: %{
        priority: 50,
        extra: [%{label: "Blog", summary: "Posts, categories, tags and their pages", fields: []}]
      }

`fields` is `[]` for an extension that needs nothing asked, and a list of
`%{name:, label:, type:, required?:}` for one that does — the installer draws them and hands the
answers back in `install/4`.

## Installing

`install/4` fires the event once. Every subscribed plugin sees it and ignores a `key` that is not
its own; the one whose key it is appends its report to `:reports`. That is the plain
`MishkaInstaller` transform chain — the payload goes out, comes back, and what came back is what
the screen shows.

## The payload

    %{
      site_id: "…",   # the site to install onto
      key: "…",       # whose install this is; a plugin ignores any other
      answers: %{},   # what the operator typed for this extension's own `fields`
      reports: [],    # where the plugin whose key it is appends its report
      ask: :install,  # what is asked: an install, the only question there is
      actor: user     # OR `nil`
    }

`:actor` IS PART OF THE CONTRACT, not a convenience. A plugin must hand it to whatever writes —
`MishkaCmsCore.Runtime.ExtensionProvision.ensure!/4` takes it as `actor:` — so the rows land under
the resource's own policy and the version history says who asked for them. `nil` means there is no
session behind the call, which is true of the installer wizard and of the dev seeds, and those
write as the system. The extension's permission names are the one exception: a name is shared by
every site and creating one is a master's action, so the system writes them whoever asked.

# `entry`

```elixir
@type entry() :: %{
  key: String.t(),
  app: atom(),
  plugin: module(),
  label: String.t(),
  summary: String.t(),
  fields: [map()],
  status: atom(),
  priority: integer()
}
```

One extension that can install itself. `key` is the OTP app as a string, which is what
`install/4` is given back and what a plugin matches on.

# `active?`

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

Whether `app` is switched on for `site_id` — `true` unless somebody turned it off.

A site that has never been set up is still ACTIVE: the switch says whether the extension is
wanted here, and a site that wants it but has not run the installer is exactly who the installer
screen is for.

# `active_key?`

```elixir
@spec active_key?(String.t()) :: boolean()
```

Whether the settings key `key` is an extension's `active?/2` switch, as `settings/2` declares it.

# `event`

```elixir
@spec event() :: String.t()
```

The event an extension subscribes to in order to be installable.

# `install`

```elixir
@spec install(String.t(), String.t() | atom(), map(), map() | nil) ::
  {:ok, MishkaCmsCore.Runtime.PageProvision.report() | map()} | {:error, term()}
```

Asks the extension named by `key` to install itself onto `site_id`, and returns what it did.

`answers` are the values the operator typed for the extension's own `fields`, empty for one that
asked for none. The return is the extension's report — `%{permissions:, components:, pages:,
snippets:, renamed_pages:, skipped_pages:}` — or `{:error, reason}` when no plugin answered to that key, so
a caller can say "nothing installed it" rather than "it worked and did nothing".

# `installable`

```elixir
@spec installable() :: [entry()]
```

Every extension registered against the event, whatever its status, in the order they are offered.

Use `offered/0` for the list a picker should draw; this one is for a screen that has to explain
why something is missing from it.

# `installed?`

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

Whether `site_id` already has `app` — which is what sends an admin to the dashboard rather than
the installer.

The record `mark_installed!/4` writes after an install is the whole answer: a site carrying no
record has never been set up with `app`, whatever pages it holds.

# `installed_version`

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

The version `site_id` was set up with, or `nil` for a site that never was.

# `mark_installed!`

```elixir
@spec mark_installed!(String.t(), atom() | String.t(), String.t() | nil, map() | nil) ::
  :ok
```

Records that `site_id` has been set up with `app`, at `version`.

Written after the install rather than before it, so a run that failed halfway leaves the site
looking like what it is — not set up — and the menu keeps offering to finish the job.

# `offered`

```elixir
@spec offered() :: [entry()]
```

The extensions a picker should draw — `installable/0` without the ones nothing would reach.

# `settings`

```elixir
@spec settings(atom(), String.t()) :: [map()]
```

The settings an extension's `settings:register` plugin should add so its own tab carries both of
the per-site answers this module reads.

    Settings.definitions() ++ Extensions.settings(:mishka_blog, "blog")

There are two of them and they are not the same question:

  * `<app>.installed` is STATE — the version this site was set up at, or blank for a site that
    never was. It is what decides whether the extension's menu opens on its dashboard or on its
    installer, so it is not editable: an admin typing a version into it would claim an install
    that never happened. `mark_installed!/4` writes it.
  * `<app>.active` is a PREFERENCE — the switch that hides the extension from this site. Off is
    not an uninstall: nothing is deleted, the menu is simply not drawn, and switching it back on
    brings everything back.

Per-site, because both answers are: one site may run the blog and another never want it.

The switch takes the setting area, as a setting declaring no permission of its own does. The
install record is written by the admin who ran the install, and setting a site up takes the site
area (`MishkaCmsCore.Runtime.ExtensionProvision.required/1`), so it takes the site area or the
setting area; the settings page shows it read-only, to them alone.

---

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