Skip to content

Admin UI Workflow

build() gives your runtime a nested, typed object. An admin UI needs the opposite: the flat list, one row per form field, with templates resolved and dropdowns populated. Two static methods cover that round trip.

The rows used below:

config rows
[
{
"accessor": "system",
"appAccess": null,
"key": "@UIColors",
"type": "string",
"listType": "csl",
"value": "Blue,Red,Green,Amber",
"options": null,
"defaultValue": null,
"editable": false,
"requiresReboot": false,
"encrypt": false,
"description": "Reusable list of allowed UI colors."
},
{
"accessor": "system",
"appAccess": null,
"key": "company.name",
"type": "string",
"listType": "none",
"value": "Acme",
"options": null,
"defaultValue": null,
"editable": true,
"requiresReboot": false,
"encrypt": false,
"description": "Display name of the operator."
},
{
"accessor": "system",
"appAccess": null,
"key": "device.port",
"type": "integer",
"listType": "none",
"value": "8080",
"options": null,
"defaultValue": "8080",
"editable": false,
"requiresReboot": true,
"encrypt": false,
"description": "Port the device listens on. Reboot to apply."
},
{
"accessor": "system",
"appAccess": null,
"key": "device.config.updateOnBoot",
"type": "boolean",
"listType": "none",
"value": "true",
"options": null,
"defaultValue": "true",
"editable": true,
"requiresReboot": false,
"encrypt": false,
"description": "Whether the device refreshes its config on boot."
},
{
"accessor": "system",
"appAccess": null,
"key": "device.config.lifeSpan",
"type": "integer",
"listType": "none",
"value": "1440",
"options": null,
"defaultValue": "1440",
"editable": true,
"requiresReboot": false,
"encrypt": false,
"description": "Config validity window, in minutes."
},
{
"accessor": "system",
"appAccess": null,
"key": "device.ui.accent",
"type": "string",
"listType": "none",
"value": "Amber",
"options": "${@UIColors}",
"defaultValue": "Blue",
"editable": true,
"requiresReboot": false,
"encrypt": false,
"description": "Accent color, constrained to the @UIColors list."
},
{
"accessor": "system",
"appAccess": null,
"key": "device.protocol.transports",
"type": "string",
"listType": "array",
"value": "http,mqtt",
"options": null,
"defaultValue": null,
"editable": true,
"requiresReboot": true,
"encrypt": false,
"description": "Enabled transports, parsed into a string array."
},
{
"accessor": "system",
"appAccess": null,
"key": "environment.host",
"type": "string",
"listType": "none",
"value": "device.local",
"options": null,
"defaultValue": null,
"editable": true,
"requiresReboot": false,
"encrypt": false,
"description": "Base hostname for device endpoints."
},
{
"accessor": "system",
"appAccess": null,
"key": "device.diagnostics.uploadUrl",
"type": "string",
"listType": "none",
"value": "https://${environment.host}/upload",
"options": null,
"defaultValue": null,
"editable": true,
"requiresReboot": false,
"encrypt": false,
"description": "Diagnostics endpoint, templated off environment.host."
},
{
"accessor": "system",
"appAccess": null,
"key": "db.password",
"type": "string",
"listType": "none",
"value": "enc:5f3a9c2e7b104d8f",
"options": null,
"defaultValue": null,
"editable": true,
"requiresReboot": false,
"encrypt": true,
"description": "Database password, stored encrypted at rest."
}
]

Configuard.parseFlat(configList) returns the same flat list (not a nested object) with:

  • every ${...} placeholder in value resolved — the value stays a string (it is not cast to its type);
  • every @-key option list extracted into a separate @ object, each a trimmed, uncast string array; and
  • every options reference expanded into that array.
import { Configuard } from 'configuard';
const { '@': optionLists, configList } = Configuard.parseFlat(rows);
optionLists;
// { UIColors: ['Blue', 'Red', 'Green', 'Amber'] }
configList.find(i => i.key === 'device.ui.accent');
// { key: 'device.ui.accent', value: 'Amber',
// options: ['Blue', 'Red', 'Green', 'Amber'], listType: 'none', … }
configList.find(i => i.key === 'device.diagnostics.uploadUrl');
// { …, value: 'https://device.local/upload', … } ${environment.host} resolved

Each row now carries everything a field needs: the current value, the allowed options (if any), and its metadata (editable, description, type).

Configuard.serializeFlat(configList, edits, options?) is the inverse: it turns the admin’s edits back into DB-ready rows. For each edit it enforces editable, validates the value against its type and options, serializes it to the storage string, optionally re-encrypts encrypt: true values, and returns the diff of changed rows.

const { updates, requiresReboot } = Configuard.serializeFlat(rows, {
'device.ui.accent': { value: 'Blue' },
'device.config.lifeSpan': { value: '720' },
'db.password': { value: 'newSecret' } // encrypt:true → re-encrypted below
}, {
encrypt: (value, item) => myEncrypt(value) // for encrypt:true items
});
// updates: [
// { key: 'device.ui.accent', id, value: 'Blue', requiresReboot: false },
// { key: 'device.config.lifeSpan', id, value: '720', requiresReboot: false },
// { key: 'db.password', id, value: '<encrypted>', requiresReboot: false }
// ]
// requiresReboot: false (true if any changed row requires it)
  • edits maps a config key to its changed fields (Partial<IConfigItem> — a value and/or metadata such as editable). Only keys you pass are processed; every other row is left untouched, so ${...} templates in unedited rows are preserved.
  • The result is { updates, requiresReboot }updates is the changed rows ({ key, id, value, requiresReboot }); requiresReboot is the aggregate. Pass { diffOnly: false } to also receive the full merged rows.

Serialization is validate-then-store: numbers and booleans are canonicalized, lists are comma-joined, and other types (hex/date/time/regexp/json/datetime) are stored as the validated string. An invalid value, an option-list violation, a change to a non-editable item, or an encrypt-hook error throws a ConfiguardError.

build() (constructor)parseFlat()
Output shapeNested objectFlat list (+ @ option lists)
valueParsed/cast to typeResolved template, kept as string
@-keysExcludedKept (in the list and @)
optionsIgnoredExpanded to string arrays
ABAC filteringYesNo