The expression engine
Genie fields and grid columns carry small expressions — a field is required only when another
field has a certain value, a cell is styled by its status, a total is computed from line items. These
run on the client through an eval-free expression engine (in expression/), and the backend ships
a matching C# port so the same rules are enforced server-side, not merely hinted.
The syntax mirrors the SQL-flavoured predicates authors already write in XML (@Field, LIKE, IN,
AND/OR/NOT, If(...)) — see Column & field expressions for the
authoring reference. This page describes the client runtime.
CSP-safe by construction
Section titled “CSP-safe by construction”There is no eval and no Function anywhere in the engine. expression/interpreter.ts is a
hand-rolled tokenizer, a recursive-descent parser, and a tree-walker over a closed grammar — so a
strict Content-Security-Policy (no unsafe-eval) never blocks it, and a malicious expression can only
reach the values and functions explicitly placed in its scope.
Two safety properties are baked in:
- Forbidden member keys — accessing
__proto__,constructor, orprototypethrows, so an expression can’t walk the prototype chain to reach a global. - Undefined identifiers throw, but callers (below) fail closed by catching the error and
treating the result as
false/ no-change — a typo disables a rule rather than crashing the form.
Parsed ASTs are cached (LRU, 256 entries) so re-evaluating the same expression as values change is cheap.
From SQL syntax to the interpreter
Section titled “From SQL syntax to the interpreter”expression/evaluate.ts first rewrites the Genie SQL-flavoured source into the interpreter’s JS-like
grammar (transformSqlToJs), then runs it:
| Authored (SQL-flavoured) | Becomes |
|---|---|
@Field / Field |
Field (bare identifier, resolved from scope) |
A = B |
A == B (loose equality, matching the legacy engine) |
A <> B |
A != B |
AND / OR / NOT |
&& / || / ! |
Field LIKE 'ab%' |
__likeMatch(Field, 'ab%') |
Field NOT LIKE 'ab%' |
!__likeMatch(Field, 'ab%') |
Field IN (A, B) |
(Field == 'A' || Field == 'B') |
Field NOT IN (A, B) |
(Field != 'A' && Field != 'B') |
LIKE uses SQL wildcard semantics — % = any run, _ = one char — and is case-insensitive.
Supported operators
Section titled “Supported operators”The interpreter grammar (in precedence order, low to high) supports:
- Logical
||,&&(short-circuiting) - Equality
==,!= - Comparison
<,<=,>,>= - Arithmetic
+,-,*,/,% - Unary
!,-,+ - Grouping
( … ), member accessa.b/a["b"], and function callsf(a, b) - Literals: numbers, single/double-quoted strings (with
\n \r \t \\ \" \'escapes),true,false,null,undefined
Equality is intentionally loose (== / !=) so @Qty = '0' matches a numeric 0, mirroring the
server port exactly.
Built-in functions
Section titled “Built-in functions”Available to every expression scope (evaluate.ts):
| Function | Purpose |
|---|---|
If(cond, a, b) (aliases IIF, iif) |
Conditional — mirrors the DataColumn IIF authors know. Both branches are eagerly evaluated (safe for the literal/field-reference branches these expressions use). |
sumLines(cart, 'QtyCol', 'PriceCol') |
Sums Qty × Price across a <CartTable> value (a JSON array of row objects). Tolerates a missing/blank/invalid cart (→ 0). |
A host may register extra functions via handlers.expressionFunctions on createGenieApp; they
are merged over the built-ins (so a host entry can override one of the same name) and reach both grid
StyleClassesExpression and field rules. See
Render extension handlers.
What the engine drives
Section titled “What the engine drives”Field required / disabled / hidden
Section titled “Field required / disabled / hidden”resolveFieldState evaluates a field’s Required / Disabled / Hidden expressions against the
form’s current values, falling back to the field’s static base flag when an expression is absent.
Each rebuild re-runs as the user types, so a field can appear, become required, or lock live.
evaluateBoolean fails closed: any parse/eval error returns false, so a broken rule never throws
inside a render.
Field-level security (Allow / Deny)
Section titled “Field-level security (Allow / Deny)”The Locked state folds in field-level security. A field is disabled when its own Disabled rule
resolves true OR its Allow/Deny access rule (buildLockedExpression, in expression/fieldRule.ts)
resolves true. fieldRule.ts also classifies whether an Allow/Deny value is a role list
(Admin,Manager) or an expression (@Type == 'B') — the one place that decision is made, so the
client agrees with the server. Role-list locks are checked against the current user’s roles
(isRoleLocked, mirroring the server’s FieldStateResolver.IsRoleLocked).
This is a UX hint only — the server re-enforces the same Allow/Deny gate on submit and discards a denied field’s caller-supplied value. See Field & column security.
Computed values
Section titled “Computed values”evaluateValue runs a field’s ValueExpression and returns the result as the field’s string value
(numbers/booleans coerced to strings), or null on error / when absent (the caller then keeps the
field’s current value). This is how a TotalAmount field auto-calculates from cart line items via
sumLines.
Grid column styling
Section titled “Grid column styling”A column’s StyleClassesExpression is evaluated per row against the row’s cell values; its string
result (typically a gx-{key} class) is appended to the cell’s CSS classes — e.g.
If(Status = 'Overdue', 'gx-danger', 'gx-success'). The named gx- colour keys are documented under
Theming.