PSA/docs/technical/parsimmon_templating_engine.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

11 KiB

Parsimmon-Based Templating Engine for Inline Forms & Dynamic Content

1. Objective

To implement a templating engine using the existing Parsimmon dependency that allows for safe evaluation of a limited set of JavaScript-like expressions within string templates. This is primarily for processing defaultValues in form schemas and dynamic form data updates, especially where template strings can be influenced by authenticated users.

The initial supported expressions are:

  • variable (accessing contextData.variableName or top-level keys in contextData)
  • 'string_literal' (single or double quoted)
  • expression1 || expression2 (logical OR)
  • new Date(variableOrStringLiteral).toLocaleDateString()
  • new Date(variableOrStringLiteral).toLocaleString()

This approach provides flexibility for future extension while maintaining control over security. The template syntax remains ${expression}.

2. Core Implementation (server/src/utils/templateUtils.ts)

The processTemplateVariables function in server/src/utils/templateUtils.ts will be updated to use Parsimmon for parsing template strings and a custom Abstract Syntax Tree (AST) evaluator.

2.1. AST Node Types (Conceptual)

  • LiteralStringNode { type: 'LiteralString', value: string }
  • VariableNode { type: 'Variable', name: string } (e.g., contextData.someKey.path or someKey)
  • OrNode { type: 'Or', left: ASTNode, right: ASTNode }
  • DateConstructorNode { type: 'DateConstructor', argument: VariableNode | LiteralStringNode }
  • MethodCallNode { type: 'MethodCall', object: ASTNode, methodName: 'toLocaleDateString' | 'toLocaleString', arguments: ASTNode[] } (arguments will be empty for these specific date methods)
  • TemplateExpressionNode { type: 'TemplateExpression', expression: ASTNode } (represents content within ${...})
  • LiteralTextNode { type: 'LiteralText', value: string } (represents plain text outside ${...})

2.2. Parsimmon Parsers (Responsibilities)

Parsimmon parsers will be defined in server/src/utils/templateUtils.ts to construct the AST. Key parsers include:

  • StringLiteralParser: Parses single or double quoted string literals (e.g., 'hello', "world"), handling escaped quotes.
  • VariableParser: Parses variable names and dot-notation paths (e.g., myKey, contextData.user.name).
  • DateConstructorParser: Parses new Date(argument) where argument can be a variable or a string literal.
  • DateMethodCallParser: Parses method calls like .toLocaleDateString() or .toLocaleString() specifically on an AST node that should evaluate to a Date object (e.g., output of DateConstructorParser or a VariableNode expected to hold a Date).
  • OrExpressionParser: Parses expression1 || expression2, designed to be left-associative. It will use a TermParser for its operands.
  • TermParser: A helper parser representing atomic parts of an expression or those with higher precedence (e.g., literals, variables, date constructions/method calls).
  • ExpressionContentParser: The main parser for the content within ${...}. It will typically be an alternation of OrExpressionParser and TermParser to handle operator precedence and recursion.
  • TemplateExpressionParser: Parses the complete ${expression} structure, using ExpressionContentParser for the inner part.
  • LiteralTextParser: Parses plain text segments outside of the ${...} expressions.
  • MainTemplateParser: The top-level parser that consumes the entire input string, alternating between LiteralTextParser and TemplateExpressionParser to produce a list of AST nodes.

Recursive parsing (e.g., for nested expressions or operator precedence) will be handled using P.lazy() where necessary.

2.3. AST Node Evaluator (evaluateAstNode - Logic Description)

A JavaScript function, evaluateAstNode(node, contextData), will be implemented in server/src/utils/templateUtils.ts. It will take a parsed AST node and the contextData object as input and return the evaluated value. Its logic will include:

  • LiteralStringNode: Returns the string value directly.
  • VariableNode: Resolves the variable name/path against the contextData. A helper like safeGet (similar to _.get) will be used for robustly accessing potentially nested properties (e.g., user.address.city from contextData). Handles cases where contextData. prefix might be part of the variable name.
  • OrNode: Evaluates the left child. If the result is truthy (according to JavaScript's definition), it's returned. Otherwise, the right child is evaluated and its result is returned.
  • DateConstructorNode: Evaluates its argument node. The result is then passed to new Date(). Handles invalid or missing arguments by returning an "Invalid Date" object.
  • MethodCallNode:
    1. Evaluates the object node (which should result in a Date instance).
    2. Verifies that the object is indeed a valid Date.
    3. Checks if the methodName is in an allowlist (initially 'toLocaleDateString', 'toLocaleString').
    4. If valid, calls the corresponding method on the Date object and returns the result.
    5. Returns an error marker or undefined for invalid objects or disallowed methods.
  • Error Handling: The evaluator will include try-catch blocks for operations like Date construction and will return specific values (e.g., undefined, an empty string, or a special error marker like #ERROR#) or log warnings for unresolvable variables, invalid operations, or unknown AST node types. This prevents the entire templating from crashing the application.

A higher-level function, evaluateParsedTemplate(parsedNodes, contextData), will iterate through the list of nodes produced by MainTemplateParser. For TemplateExpressionNodes, it will call evaluateAstNode on the inner expression and convert the result to a string (handling null/undefined by converting to an empty string). For LiteralTextNodes, it will append their value. The results will be concatenated to form the final output string.

2.4. Main processTemplateVariables Function Update

The existing processTemplateVariables function will be refactored:

  1. It will first check if the input string value actually contains ${ to avoid unnecessary parsing.
  2. If templating is needed, it will call MainTemplateParser.parse(value).
  3. If parsing is successful (parseResult.status is true), it will pass parseResult.value (the list of AST nodes) and contextData to evaluateParsedTemplate to get the final string.
  4. If parsing fails, it will log a warning (using P.formatError for details) and return the original string to minimize disruption.
  5. Robust try-catch blocks will surround parsing and evaluation calls.
  6. The recursive processing for array and object values within processTemplateVariables will remain, ensuring that string values nested within these structures are also processed by the new Parsimmon-based logic.

3. Integration Points

This updated processTemplateVariables function will be automatically used by:

No changes are expected to be needed in these consuming components beyond ensuring they pass valid contextData.

4. Security Considerations for User-Influenced Templates

Since template expressions are influenced by authenticated users, security is paramount:

  • Strictly Defined Grammar: The Parsimmon grammar only allows the specified limited set of expressions. It does not parse or allow arbitrary JavaScript.
  • Controlled Evaluator: The evaluateAstNode function is custom-written to:
    • Scope variable lookups strictly to the provided contextData.
    • Only permit new Date(...) for object construction.
    • Only permit .toLocaleDateString() and .toLocaleString() as method calls, and only on Date instances.
  • No eval() or new Function(string): The approach explicitly avoids general-purpose JavaScript evaluation mechanisms.
  • Error Handling: Parsing and evaluation errors are caught, logged, and result in returning the original string or a safe fallback, preventing crashes and minimizing unexpected behavior.

5. Testing Strategy

  • Unit Tests for Parsers and evaluateAstNode.
  • Unit Tests for processTemplateVariables.
  • Integration Testing within DynamicForm and ActivityDetailViewerDrawer.
  • Security Review of parser and evaluator.

6. Diagram of Parsimmon Implementation

graph TD
    TemplateString["Input String e.g., '${contextData.name || 'Guest'}'"] --> MainTemplateParser;

    subgraph MainTemplateParser ["MainTemplateParser (Parsimmon)"]
        P_Alt["P.alt()"] --> P_ExprInBraces["TemplateExpressionParser (${...})"];
        P_Alt --> P_LiteralText["LiteralTextParser (plain text)"];
    end
    
    P_ExprInBraces --> ExpressionContentParser["ExpressionContentParser (Parsimmon, Recursive for content of ${...})"];
    
    subgraph ExpressionContentParser
        EP_Alt["P.alt()"] --> EP_Or["OrExpressionParser (Term || Expression)"];
        EP_Alt --> EP_Term["TermParser"];
    end

    subgraph EP_Term ["TermParser (Parsimmon)"]
        T_Alt["P.alt()"] --> T_DateMethod["DateMethodCallParser (e.g., new Date(...).toLocale...())"];
        T_Alt --> T_DateConst["DateConstructorParser (new Date(...))"];
        T_Alt --> T_Var["VariableParser (contextData.key or key)"];
        T_Alt --> T_StrLit["StringLiteralParser ('text')"];
    end

    MainTemplateParser --> AST["List of AST Nodes (LiteralTextNode | TemplateExpressionNode)"];
    AST --> EvaluateParsedTemplate["evaluateParsedTemplate()"];
    EvaluateParsedTemplate -->|For each TemplateExpressionNode| EvaluateAstNode["evaluateAstNode(expressionNode, contextData)"];
    
    subgraph EvaluateAstNode
        SwitchNode["Switch on AST Node Type"]
        SwitchNode --> HandleLiteral["Handle LiteralStringNode"];
        SwitchNode --> HandleVar["Handle VariableNode (lookup in contextData via safeGet)"];
        SwitchNode --> HandleOr["Handle OrNode (eval left, then right if needed)"];
        SwitchNode --> HandleDateConst["Handle DateConstructorNode (new Date(eval arg))"];
        SwitchNode --> HandleMethodCall["Handle MethodCallNode (eval obj, call whitelisted method on Date)"];
    end

    EvaluateAstNode --> EvaluatedSubValue["Evaluated Sub-Expression Value"];
    EvaluatedSubValue --> EvaluateParsedTemplate;
    EvaluateParsedTemplate --> FinalString["Concatenated Final String Output"];