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:
[ { "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." }]1. Render — parseFlat()
Section titled “1. Render — parseFlat()”Configuard.parseFlat(configList) returns the same flat list (not a nested
object) with:
- every
${...}placeholder invalueresolved — the value stays a string (it is not cast to itstype); - every
@-key option list extracted into a separate@object, each a trimmed, uncast string array; and - every
optionsreference 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} resolvedEach row now carries everything a field needs: the current value, the allowed
options (if any), and its metadata (editable, description, type).
2. Save — serializeFlat()
Section titled “2. Save — serializeFlat()”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)editsmaps a configkeyto its changed fields (Partial<IConfigItem>— avalueand/or metadata such aseditable). Only keys you pass are processed; every other row is left untouched, so${...}templates in unedited rows are preserved.- The result is
{ updates, requiresReboot }—updatesis the changed rows ({ key, id, value, requiresReboot });requiresRebootis the aggregate. Pass{ diffOnly: false }to also receive the full mergedrows.
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() vs parseFlat()
Section titled “build() vs parseFlat()”build() (constructor) | parseFlat() | |
|---|---|---|
| Output shape | Nested object | Flat list (+ @ option lists) |
value | Parsed/cast to type | Resolved template, kept as string |
@-keys | Excluded | Kept (in the list and @) |
options | Ignored | Expanded to string arrays |
| ABAC filtering | Yes | No |