@fogpipe/forma-core 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VHKUCCAC.js → chunk-KUZ3NPM4.js} +2 -2
- package/dist/{chunk-QTLXVG6P.js → chunk-U2OXXFEH.js} +5 -1
- package/dist/chunk-U2OXXFEH.js.map +1 -0
- package/dist/engine/index.cjs +4 -0
- package/dist/engine/index.cjs.map +1 -1
- package/dist/engine/index.js +2 -2
- package/dist/feel/index.cjs +4 -0
- package/dist/feel/index.cjs.map +1 -1
- package/dist/feel/index.d.ts.map +1 -1
- package/dist/feel/index.js +1 -1
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/src/__tests__/feel.test.ts +833 -0
- package/src/feel/index.ts +14 -0
- package/dist/chunk-QTLXVG6P.js.map +0 -1
- /package/dist/{chunk-VHKUCCAC.js.map → chunk-KUZ3NPM4.js.map} +0 -0
package/src/feel/index.ts
CHANGED
|
@@ -153,7 +153,21 @@ export function evaluateBoolean(
|
|
|
153
153
|
|
|
154
154
|
// FEEL uses three-valued logic where comparisons with null return null.
|
|
155
155
|
// For form visibility/required/enabled conditions, we treat null as false.
|
|
156
|
+
//
|
|
157
|
+
// Common causes of null results:
|
|
158
|
+
// - "fieldName = true" when fieldName is undefined → null (not false!)
|
|
159
|
+
// - "fieldName != null" when fieldName is undefined → null (not true!)
|
|
160
|
+
// - "computed.x = true" when x is null → null
|
|
161
|
+
//
|
|
162
|
+
// Null-safe alternatives:
|
|
163
|
+
// - Check if boolean answered: "fieldName = true or fieldName = false"
|
|
164
|
+
// - Check if explicitly false: "fieldName != true" (true when undefined OR false)
|
|
165
|
+
// - String length: "fieldName != null and string length(fieldName) > 0"
|
|
156
166
|
if (result.value === null || result.value === undefined) {
|
|
167
|
+
console.warn(
|
|
168
|
+
`[forma] FEEL expression returned null (treating as false): "${expression}"\n` +
|
|
169
|
+
`This often means a referenced field is undefined. See docs for null-safe patterns.`
|
|
170
|
+
);
|
|
157
171
|
return false;
|
|
158
172
|
}
|
|
159
173
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/feel/index.ts"],"sourcesContent":["/**\n * FEEL Expression Evaluator\n *\n * Wraps the feelin library to provide FEEL expression evaluation\n * with Forma context conventions.\n *\n * Context variable conventions:\n * - `fieldName` - Direct field value access\n * - `computed.name` - Computed value access\n * - `ref.path` - Reference data lookup (external lookup tables)\n * - `item.fieldName` - Array item field access (within array context)\n * - `itemIndex` - Current array item index (0-based)\n * - `value` - Current field value (in validation expressions)\n */\n\nimport { evaluate as feelinEvaluate } from \"feelin\";\nimport type { EvaluationContext, FEELExpression } from \"../types.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface EvaluateResult<T = unknown> {\n success: true;\n value: T;\n}\n\nexport interface EvaluateError {\n success: false;\n error: string;\n expression: string;\n}\n\nexport type EvaluationOutcome<T = unknown> = EvaluateResult<T> | EvaluateError;\n\n// ============================================================================\n// Context Building\n// ============================================================================\n\n/**\n * Build the FEEL evaluation context from our EvaluationContext\n *\n * Maps our context conventions to feelin context format:\n * - Form data fields are spread directly\n * - computed becomes an object (accessed as computed.fieldName)\n * - ref becomes an object for reference data (accessed as ref.path.to.data)\n * - item becomes an object for array context (accessed as item.fieldName)\n * - itemIndex is a number for array context\n * - value is the current field value\n */\nfunction buildFeelContext(ctx: EvaluationContext): Record<string, unknown> {\n const feelContext: Record<string, unknown> = {\n // Spread form data directly so fields are accessible by name\n ...ctx.data,\n };\n\n // Add computed values under 'computed' (accessed as computed.fieldName)\n if (ctx.computed) {\n feelContext[\"computed\"] = ctx.computed;\n }\n\n // Add reference data under 'ref' (accessed as ref.path.to.data)\n if (ctx.referenceData) {\n feelContext[\"ref\"] = ctx.referenceData;\n }\n\n // Add array item context (accessed as item.fieldName)\n if (ctx.item !== undefined) {\n feelContext[\"item\"] = ctx.item;\n }\n\n // Add array index\n if (ctx.itemIndex !== undefined) {\n feelContext[\"itemIndex\"] = ctx.itemIndex;\n }\n\n // Add current field value for validation expressions\n if (ctx.value !== undefined) {\n feelContext[\"value\"] = ctx.value;\n }\n\n return feelContext;\n}\n\n// ============================================================================\n// Expression Evaluation\n// ============================================================================\n\n/**\n * Evaluate a FEEL expression and return the result\n *\n * @param expression - FEEL expression string\n * @param context - Evaluation context with form data, computed values, etc.\n * @returns Evaluation outcome with success/value or error\n *\n * @example\n * // Simple field comparison\n * evaluate(\"age >= 18\", { data: { age: 21 } })\n * // => { success: true, value: true }\n *\n * @example\n * // Computed value reference\n * evaluate(\"computed.bmi > 30\", { data: {}, computed: { bmi: 32.5 } })\n * // => { success: true, value: true }\n *\n * @example\n * // Array item context\n * evaluate(\"item.frequency = \\\"daily\\\"\", { data: {}, item: { frequency: \"daily\" } })\n * // => { success: true, value: true }\n */\nexport function evaluate<T = unknown>(\n expression: FEELExpression,\n context: EvaluationContext\n): EvaluationOutcome<T> {\n try {\n const feelContext = buildFeelContext(context);\n const result = feelinEvaluate(expression, feelContext);\n return {\n success: true,\n value: result as T,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : String(error),\n expression,\n };\n }\n}\n\n/**\n * Evaluate a FEEL expression expecting a boolean result\n *\n * Used for visibility, required, and enabled conditions.\n * Returns false on error or non-boolean result for safety.\n *\n * @param expression - FEEL expression that should return boolean\n * @param context - Evaluation context\n * @returns Boolean result (false on error)\n */\nexport function evaluateBoolean(\n expression: FEELExpression,\n context: EvaluationContext\n): boolean {\n const result = evaluate<boolean>(expression, context);\n\n if (!result.success) {\n console.warn(\n `FEEL expression error: ${result.error}\\nExpression: ${result.expression}`\n );\n return false;\n }\n\n // FEEL uses three-valued logic where comparisons with null return null.\n // For form visibility/required/enabled conditions, we treat null as false.\n if (result.value === null || result.value === undefined) {\n return false;\n }\n\n if (typeof result.value !== \"boolean\") {\n console.warn(\n `FEEL expression did not return boolean: ${expression}\\nGot: ${typeof result.value}`\n );\n return false;\n }\n\n return result.value;\n}\n\n/**\n * Evaluate a FEEL expression expecting a numeric result\n *\n * Used for computed values that return numbers.\n *\n * @param expression - FEEL expression that should return number\n * @param context - Evaluation context\n * @returns Numeric result or null on error\n */\nexport function evaluateNumber(\n expression: FEELExpression,\n context: EvaluationContext\n): number | null {\n const result = evaluate<number>(expression, context);\n\n if (!result.success) {\n console.warn(\n `FEEL expression error: ${result.error}\\nExpression: ${result.expression}`\n );\n return null;\n }\n\n if (typeof result.value !== \"number\") {\n console.warn(\n `FEEL expression did not return number: ${expression}\\nGot: ${typeof result.value}`\n );\n return null;\n }\n\n return result.value;\n}\n\n/**\n * Evaluate a FEEL expression expecting a string result\n *\n * @param expression - FEEL expression that should return string\n * @param context - Evaluation context\n * @returns String result or null on error\n */\nexport function evaluateString(\n expression: FEELExpression,\n context: EvaluationContext\n): string | null {\n const result = evaluate<string>(expression, context);\n\n if (!result.success) {\n console.warn(\n `FEEL expression error: ${result.error}\\nExpression: ${result.expression}`\n );\n return null;\n }\n\n if (typeof result.value !== \"string\") {\n console.warn(\n `FEEL expression did not return string: ${expression}\\nGot: ${typeof result.value}`\n );\n return null;\n }\n\n return result.value;\n}\n\n// ============================================================================\n// Batch Evaluation\n// ============================================================================\n\n/**\n * Evaluate multiple FEEL expressions at once\n *\n * Useful for evaluating all visibility conditions in a form.\n *\n * @param expressions - Map of field names to FEEL expressions\n * @param context - Evaluation context\n * @returns Map of field names to boolean results\n */\nexport function evaluateBooleanBatch(\n expressions: Record<string, FEELExpression>,\n context: EvaluationContext\n): Record<string, boolean> {\n const results: Record<string, boolean> = {};\n\n for (const [key, expression] of Object.entries(expressions)) {\n results[key] = evaluateBoolean(expression, context);\n }\n\n return results;\n}\n\n// ============================================================================\n// Expression Validation\n// ============================================================================\n\n/**\n * Check if a FEEL expression is syntactically valid\n *\n * @param expression - FEEL expression to validate\n * @returns True if the expression can be parsed\n */\nexport function isValidExpression(expression: FEELExpression): boolean {\n try {\n // Try to evaluate with empty context - we just want to check parsing\n feelinEvaluate(expression, {});\n return true;\n } catch {\n // Expression failed to parse or evaluate\n return false;\n }\n}\n\n/**\n * Validate a FEEL expression and return any parsing errors\n *\n * @param expression - FEEL expression to validate\n * @returns Null if valid, error message if invalid\n */\nexport function validateExpression(expression: FEELExpression): string | null {\n try {\n // Evaluate with minimal context to catch parse errors\n feelinEvaluate(expression, {});\n return null;\n } catch (error) {\n // Only return actual parsing errors, not runtime errors\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes(\"parse\") || message.includes(\"syntax\")) {\n return message;\n }\n // Runtime errors (missing variables, etc.) are OK for validation\n return null;\n }\n}\n"],"mappings":";AAeA,SAAS,YAAY,sBAAsB;AAmC3C,SAAS,iBAAiB,KAAiD;AACzE,QAAM,cAAuC;AAAA;AAAA,IAE3C,GAAG,IAAI;AAAA,EACT;AAGA,MAAI,IAAI,UAAU;AAChB,gBAAY,UAAU,IAAI,IAAI;AAAA,EAChC;AAGA,MAAI,IAAI,eAAe;AACrB,gBAAY,KAAK,IAAI,IAAI;AAAA,EAC3B;AAGA,MAAI,IAAI,SAAS,QAAW;AAC1B,gBAAY,MAAM,IAAI,IAAI;AAAA,EAC5B;AAGA,MAAI,IAAI,cAAc,QAAW;AAC/B,gBAAY,WAAW,IAAI,IAAI;AAAA,EACjC;AAGA,MAAI,IAAI,UAAU,QAAW;AAC3B,gBAAY,OAAO,IAAI,IAAI;AAAA,EAC7B;AAEA,SAAO;AACT;AA4BO,SAAS,SACd,YACA,SACsB;AACtB,MAAI;AACF,UAAM,cAAc,iBAAiB,OAAO;AAC5C,UAAM,SAAS,eAAe,YAAY,WAAW;AACrD,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF,SAAS,OAAO;AACd,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,MAC5D;AAAA,IACF;AAAA,EACF;AACF;AAYO,SAAS,gBACd,YACA,SACS;AACT,QAAM,SAAS,SAAkB,YAAY,OAAO;AAEpD,MAAI,CAAC,OAAO,SAAS;AACnB,YAAQ;AAAA,MACN,0BAA0B,OAAO,KAAK;AAAA,cAAiB,OAAO,UAAU;AAAA,IAC1E;AACA,WAAO;AAAA,EACT;AAIA,MAAI,OAAO,UAAU,QAAQ,OAAO,UAAU,QAAW;AACvD,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,OAAO,UAAU,WAAW;AACrC,YAAQ;AAAA,MACN,2CAA2C,UAAU;AAAA,OAAU,OAAO,OAAO,KAAK;AAAA,IACpF;AACA,WAAO;AAAA,EACT;AAEA,SAAO,OAAO;AAChB;AAWO,SAAS,eACd,YACA,SACe;AACf,QAAM,SAAS,SAAiB,YAAY,OAAO;AAEnD,MAAI,CAAC,OAAO,SAAS;AACnB,YAAQ;AAAA,MACN,0BAA0B,OAAO,KAAK;AAAA,cAAiB,OAAO,UAAU;AAAA,IAC1E;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,OAAO,UAAU,UAAU;AACpC,YAAQ;AAAA,MACN,0CAA0C,UAAU;AAAA,OAAU,OAAO,OAAO,KAAK;AAAA,IACnF;AACA,WAAO;AAAA,EACT;AAEA,SAAO,OAAO;AAChB;AASO,SAAS,eACd,YACA,SACe;AACf,QAAM,SAAS,SAAiB,YAAY,OAAO;AAEnD,MAAI,CAAC,OAAO,SAAS;AACnB,YAAQ;AAAA,MACN,0BAA0B,OAAO,KAAK;AAAA,cAAiB,OAAO,UAAU;AAAA,IAC1E;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,OAAO,UAAU,UAAU;AACpC,YAAQ;AAAA,MACN,0CAA0C,UAAU;AAAA,OAAU,OAAO,OAAO,KAAK;AAAA,IACnF;AACA,WAAO;AAAA,EACT;AAEA,SAAO,OAAO;AAChB;AAeO,SAAS,qBACd,aACA,SACyB;AACzB,QAAM,UAAmC,CAAC;AAE1C,aAAW,CAAC,KAAK,UAAU,KAAK,OAAO,QAAQ,WAAW,GAAG;AAC3D,YAAQ,GAAG,IAAI,gBAAgB,YAAY,OAAO;AAAA,EACpD;AAEA,SAAO;AACT;AAYO,SAAS,kBAAkB,YAAqC;AACrE,MAAI;AAEF,mBAAe,YAAY,CAAC,CAAC;AAC7B,WAAO;AAAA,EACT,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAQO,SAAS,mBAAmB,YAA2C;AAC5E,MAAI;AAEF,mBAAe,YAAY,CAAC,CAAC;AAC7B,WAAO;AAAA,EACT,SAAS,OAAO;AAEd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,QAAI,QAAQ,SAAS,OAAO,KAAK,QAAQ,SAAS,QAAQ,GAAG;AAC3D,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
File without changes
|