Skip to content

Technical

Components in the Add-on Installer

The following components are included in the add-on installer:

  • Web components
  • Installer config
  • Internal endpoints

Web Components

The add-on installer contains the web components to show the installer config form and the installation dialog. These components are automatically rendered in Lime Admin after successfully implementing it in an add-on as described here.

Installer Config

The installer config is stored in Lime Data under the key addon_installer_config. The installer config is saved there to make it possible to populate the default value for corresponding properties in the runtime configuration. The add-on installer's BaseInstaller class contains functions that can be used to fetch values from the installer config.

It is saved when installing, never when only verifying — the stored config also seeds default values for the runtime configuration, so a config that was never actually applied must not end up there.

The save happens once, when the user confirms the installation, before any installation step is dispatched. It therefore happens regardless of which steps turn out to have work to do. Previously it was written by the database structure step, which the frontend skips entirely when there is nothing to change — so add-ons without a database structure never stored their config, and re-installations never refreshed it.

Because it lands before the steps run, addon_installer_config[<storage_key>] means "the config the last installation was started with", not "the config that was last installed successfully": a run that fails partway still leaves its config stored. That is deliberate — it is the config the user asked for, and it is what they will want prefilled when they retry.

The monolithic /install/ endpoint still writes the config itself, since it dispatches no separate steps. The setup page uses /install/step/, so in practice the write comes from the dialog.

Alongside it, <storage_key>__meta records who saved the config and when. This is kept in a sibling key rather than inside the config itself, since the config dict is passed verbatim to the consumer's get_installation_data.

The setup form is prefilled with the stored config on load, so the same values do not have to be entered again.

The form is not rendered until that fetch has settled; a spinner and a short message take its place in the meantime. Rendering the form first and seeding it afterwards would mean deciding whether the user had started typing, and limel-form reports a change while it applies the schema's own defaults, so there is no reliable way to tell the two apart. If the fetch fails or times out, the form is rendered empty, exactly as it behaved before the prefill existed, and it is not tried again: by then the form is on screen and may have been typed into, so a later response would replace the user's input with the stored config. One attempt, made before the form exists, is what keeps the prefill from ever overwriting anything.

Before it is used, the endpoint below validates the stored config against the add-on's current installer config schema. The schema is fetched from the add-on's Lime Admin plugin — the same get_schema call that builds the setup form — so the config is checked against exactly what the form was generated from, whether the consumer passed a custom installer config schema or let one be derived from their admin config schema.

The stored config is returned either way, together with whatever did not load — marshmallow's own error dict, handed on verbatim. If there is anything in it, typically because a newer version of the add-on removed or changed a field, the setup page leaves those values out of the prefill and keeps the rest. Seeding a value that does not load would disable the Check installation button; blanking the whole config instead would throw away every value the user would have kept.

The user is only told that some settings were left out, never which or why. Marshmallow's messages are written for whoever is debugging a schema, not for the person installing an add-on, and they are visible in the request for exactly that purpose. The setup page only reads the top-level field names, to know what to drop.

Deserializing runs the consumer's own field code against live CRM state, which can fail in ways that are not validation errors. Those are logged and the config is returned unvalidated rather than failing the request; the form's own validation remains the backstop.

This is marshmallow's verdict and nothing more. It is deliberately not an attempt to predict the form's: the form validates with Ajv against the JSON schema generated from that same marshmallow schema, and the two disagree in both directions — an EXCLUDE schema drops an unknown key silently while the generated schema forbids it, and marshmallow coerces "3" to 3 where the generated schema says integer. Mirroring those rules on the backend was unbounded and kept missing cases, so the form gets the last word instead.

What happens when the form disagrees depends on whether the user can do anything about it. A value in a field the form renders is theirs to correct, and the form already marks it up inline — the prefill notice is left alone. A value under a key the schema no longer has is different: nothing is rendered for it, so there is no way to clear it from the page. Only that case turns the notice into a warning and offers a Discard saved settings button, which empties the form. Ajv reports it as additionalProperties, which is what the setup page keys on; it cannot come from anything the user typed, since they can only edit fields that exist. The form's own error list is written to the browser console with console.debug rather than shown.

That escape hatch is what keeps a stale config from ever leaving the form permanently un-installable, without the backend having to predict Ajv's verdict.

Values stored under a credential-shaped key name are withheld from the response and listed in omitted, using the same key names the artifact download redacts. They are dropped rather than replaced with a placeholder, so the field renders empty and the user retypes it instead of saving a placeholder back on the next installation.

If no schema can be found for the storage key, the config is returned without errors and the form's own validation remains the backstop.

A missing required value is the one exception, and is not treated as a reason to discard the config: the form renders that field and asks for it, so the user can recover from it directly.

Note

Lime Data is not encrypted, and the stored config is returned by the endpoint below. That endpoint is restricted to administrators, unlike Lime Admin's own schema and config endpoints, which any authenticated user can read.

How the installer config works is described here.

Internal Endpoints

The following endpoints are used in this add-on:

Install

POST

/install/

This endpoint can both be used for analyzing and installing.

Analyze:

If is_analyze is true, an analysis is performed of the changes resulting from the installation data being applied to the database. The changes won't actually be applied.

If is_analyze is false the installation data will be applied to the database.

Payload:

{
    installation_module: "<path/to/database-structure/module>",
    storage_key: "<storage-key>",
    installation_config: dict,
    is_analyze: bool
}

Response:

{
    "database_structure": {
        "tables": [
            {
                "view": {}
            }
        ],
        "fields": [],
        "relations": [],
        "installed": true/false,
        "views_installed": true/false
    },
    "security": {
        "users": [],
        "groups": [],
        "installed": true/false
    }
}

Config

Reads and writes the installer config the user fills in on the setup page. Both methods are restricted to administrators.

GET

/config/<storage-key>/

Returns the stored installer config, used to prefill the setup form.

Response:

{
    "config": dict,
    "errors": {"<field>": ["<message>"]},
    "omitted": ["<dotted path>"],
    "read_failed": bool,
    "saved_at": "<iso-8601 timestamp>",
    "saved_by": "<username>"
}

config is {} when nothing has been saved for the storage key yet, in which case saved_at and saved_by are null.

errors is ValidationError.messages as marshmallow produced it: keyed by field name and nested to mirror the config, so a bad value inside a list of nested objects arrives as {"limetypes": {"0": {"name": ["..."]}}}. It is {} when the config can be used as-is. A missing required value is deliberately not reported — the form renders that field and asks for it, so the user can recover from it directly.

omitted lists the paths withheld because their key name looks like a credential. Nothing is wrong with them; they have to be entered again.

read_failed is true when the stored config could not be read at all, as opposed to not existing. The setup page distinguishes the two: an unexplained empty form would invite the user to retype a config that is still there.

POST

/config/

Saves the installer config. Called once, from the installation dialog, when the user confirms the installation.

Payload:

{
    storage_key: "<storage-key>",
    installation_config: dict
}

installation_config must be an object. An empty one is ignored rather than written, so an add-on whose installer config schema was removed cannot silently blank what is stored.

storage_key must not end in __meta or __syscfg. Those suffixes mark the bookkeeping keys stored beside a config — the saved-by attribution and the system-configuration provider state — and accepting them would let a request forge attribution or corrupt installation state.

Response:

{
    "saved": bool
}

saved is false both when there was nothing to save and when the write failed; the failure is logged with a traceback.

saved is false when there was nothing to save — an empty installation_config is ignored rather than written, so that an add-on whose installer config schema has been removed does not blank what is already stored. It is also false when the config could not be written; saving is a convenience and never blocks an installation.