Skip to content

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.

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, or prototype throws, 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.

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.

The interpreter grammar (in precedence order, low to high) supports:

  • Logical ||, && (short-circuiting)
  • Equality ==, !=
  • Comparison <, <=, >, >=
  • Arithmetic +, -, *, /, %
  • Unary !, -, +
  • Grouping ( … ), member access a.b / a["b"], and function calls f(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.

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.

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.

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.

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.

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.