From 1c76ebc92fd559795c12aff385ad63b7afc2fab4 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Fri, 8 May 2026 12:20:45 +0530 Subject: [PATCH 1/4] fix(orchestrator-form-widgets): handle missing fetch value selectors safely Treat null or undefined JSONata results as empty string for ActiveTextInput fetch response values to avoid UI breakage while editing dynamic selector keys, and add tests for applySelectorString strict/lenient behavior. --- .../active-textinput-missing-selector.md | 5 +++ .../src/utils/applySelector.test.ts | 36 ++++++++++++++++++- .../src/utils/applySelector.ts | 5 +++ .../src/widgets/ActiveTextInput.tsx | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 workspaces/orchestrator/.changeset/active-textinput-missing-selector.md diff --git a/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md b/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md new file mode 100644 index 0000000000..8c738dbcb4 --- /dev/null +++ b/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-orchestrator-form-widgets': patch +--- + +Avoid hard failures in ActiveTextInput when `fetch:response:value` points to a missing response property. If JSONata resolves to `undefined` or `null`, treat it as an empty string so retriggered dynamic selector edits do not break the form UI. diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts index acca77ec0d..1f714a8800 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { applySelectorArray } from './applySelector'; +import { applySelectorArray, applySelectorString } from './applySelector'; import { JsonObject } from '@backstage/types'; describe('applySelectorArray', () => { @@ -258,6 +258,40 @@ describe('applySelectorArray', () => { }); }); +describe('applySelectorString', () => { + const data: JsonObject = { status: 'UP', nested: { name: 'x' } }; + + it('returns string when selector evaluates to a string', async () => { + await expect(applySelectorString(data, 'status')).resolves.toBe('UP'); + }); + + it('throws when selector is missing and emptyStringWhenMissing is false', async () => { + await expect(applySelectorString(data, 'doesNotExist')).rejects.toThrow( + 'Unexpected result of "doesNotExist" selector, expected string type', + ); + }); + + it('returns empty string when selector is missing and emptyStringWhenMissing is true', async () => { + await expect(applySelectorString(data, 'doesNotExist', true)).resolves.toBe( + '', + ); + }); + + it('returns empty string when JSONata yields null and emptyStringWhenMissing is true', async () => { + const withNull: JsonObject = { absent: null }; + await expect(applySelectorString(withNull, 'absent', true)).resolves.toBe( + '', + ); + }); + + it('still throws for non-string non-nullish when emptyStringWhenMissing is true', async () => { + const withNum: JsonObject = { n: 42 }; + await expect(applySelectorString(withNum, 'n', true)).rejects.toThrow( + 'expected string type', + ); + }); +}); + describe('applySelectorArray - complex queries', () => { it('handles complex queries', async () => { const data = [ diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts index dcff1d0c3a..ea7b510b34 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts @@ -46,6 +46,7 @@ export const applySelectorArray = async ( export const applySelectorString = async ( data: JsonObject, selector: string, + emptyStringWhenMissing: boolean = false, ): Promise => { const expression = jsonata(selector); const value = await expression.evaluate(data); @@ -54,6 +55,10 @@ export const applySelectorString = async ( return value; } + if (emptyStringWhenMissing && (value === undefined || value === null)) { + return ''; + } + throw new Error( `Unexpected result of "${selector}" selector, expected string type. Value "${JSON.stringify(value)}"`, ); diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx index cbc7197b0f..04bc568d78 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/ActiveTextInput.tsx @@ -153,6 +153,7 @@ export const ActiveTextInput: Widget< const fetchedValue = await applySelectorString( data, resolvedSelector, + true, ); if ( From 9d84ed263293434fae32adbd75b415a6202657f0 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Mon, 11 May 2026 13:20:32 +0530 Subject: [PATCH 2/4] fix(orchestrator-form-widgets): tolerate invalid JSONata while editing selectors Wrap JSONata compile/evaluate in applySelector with clear errors and lenient empty string/array fallbacks where already used for fetch responses. Skip invalid jsonata: body fields in useRequestInit instead of failing the whole request. Extend changeset and tests. Co-authored-by: Cursor --- .../active-textinput-missing-selector.md | 2 + .../src/utils/applySelector.test.ts | 34 +++++++++ .../src/utils/applySelector.ts | 70 +++++++++++++++++-- .../src/utils/useRequestInit.test.ts | 22 ++++++ .../src/utils/useRequestInit.ts | 13 +++- 5 files changed, 132 insertions(+), 9 deletions(-) diff --git a/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md b/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md index 8c738dbcb4..34bdc14892 100644 --- a/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md +++ b/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md @@ -3,3 +3,5 @@ --- Avoid hard failures in ActiveTextInput when `fetch:response:value` points to a missing response property. If JSONata resolves to `undefined` or `null`, treat it as an empty string so retriggered dynamic selector edits do not break the form UI. + +Wrap JSONata compile and evaluation errors for fetch response selectors: invalid or partial expressions (e.g. lone `.` or `/` while typing) return empty string or empty options where lenient modes apply, and strict callers get clearer `Invalid JSONata` / evaluation error messages. `jsonata:` values in `fetch:body` / `validate:body` that fail to compile or evaluate are skipped instead of breaking the whole request init. diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts index 1f714a8800..d2a5705b38 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts @@ -290,6 +290,40 @@ describe('applySelectorString', () => { 'expected string type', ); }); + + it('returns empty string for invalid JSONata syntax when emptyStringWhenMissing is true', async () => { + await expect( + applySelectorString({} as JsonObject, '.', true), + ).resolves.toBe(''); + await expect( + applySelectorString({} as JsonObject, '/', true), + ).resolves.toBe(''); + }); + + it('throws a clear error for invalid JSONata syntax when strict', async () => { + await expect(applySelectorString({} as JsonObject, '.')).rejects.toThrow( + 'Invalid JSONata', + ); + }); +}); + +describe('applySelectorArray invalid JSONata', () => { + const data: JsonObject = { args: { tag: ['a'] } }; + + it('returns empty array for invalid syntax when emptyArrayIfNeeded is true', async () => { + await expect( + applySelectorArray(data, '.', true, true), + ).resolves.toStrictEqual([]); + await expect( + applySelectorArray(data, '/', true, true), + ).resolves.toStrictEqual([]); + }); + + it('throws for invalid syntax when not lenient', async () => { + await expect(applySelectorArray(data, '.')).rejects.toThrow( + 'Invalid JSONata', + ); + }); }); describe('applySelectorArray - complex queries', () => { diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts index ea7b510b34..99289f7eb6 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts @@ -17,14 +17,46 @@ import jsonata from 'jsonata'; import { JsonArray, JsonObject } from '@backstage/types'; import { isJsonObject } from '@red-hat-developer-hub/backstage-plugin-orchestrator-common'; +const jsonataReason = (reason: unknown): string => + reason instanceof Error ? reason.message : String(reason); + +const compileJsonata = (selector: string) => { + try { + return jsonata(selector); + } catch (reason) { + throw new Error( + `Invalid JSONata selector ${JSON.stringify(selector)}: ${jsonataReason(reason)}`, + ); + } +}; + export const applySelectorArray = async ( data: JsonObject | JsonArray, selector: string, createArrayIfNeeded: boolean = false, emptyArrayIfNeeded: boolean = false, ): Promise => { - const expression = jsonata(selector); - const value = await expression.evaluate(data); + let expression; + try { + expression = compileJsonata(selector); + } catch (reason) { + if (emptyArrayIfNeeded) { + return []; + } + throw reason; + } + + let value; + try { + value = await expression.evaluate(data); + } catch (reason) { + if (emptyArrayIfNeeded) { + return []; + } + throw new Error( + `JSONata evaluation failed for ${JSON.stringify(selector)}: ${jsonataReason(reason)}`, + ); + } if (emptyArrayIfNeeded && !value) { return []; @@ -48,8 +80,27 @@ export const applySelectorString = async ( selector: string, emptyStringWhenMissing: boolean = false, ): Promise => { - const expression = jsonata(selector); - const value = await expression.evaluate(data); + let expression; + try { + expression = compileJsonata(selector); + } catch (reason) { + if (emptyStringWhenMissing) { + return ''; + } + throw reason; + } + + let value; + try { + value = await expression.evaluate(data); + } catch (reason) { + if (emptyStringWhenMissing) { + return ''; + } + throw new Error( + `JSONata evaluation failed for ${JSON.stringify(selector)}: ${jsonataReason(reason)}`, + ); + } if (typeof value === 'string') { return value; @@ -68,8 +119,15 @@ export const applySelectorObject = async ( data: JsonObject, selector: string, ): Promise => { - const expression = jsonata(selector); - const value = await expression.evaluate(data); + const expression = compileJsonata(selector); + let value; + try { + value = await expression.evaluate(data); + } catch (reason) { + throw new Error( + `JSONata evaluation failed for ${JSON.stringify(selector)}: ${jsonataReason(reason)}`, + ); + } if (isJsonObject(value)) { return value; diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useRequestInit.test.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useRequestInit.test.ts index 1d4c4339d7..318d39f903 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useRequestInit.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useRequestInit.test.ts @@ -95,4 +95,26 @@ describe('getRequestInit', () => { name: 'Zara', }); }); + + it('omits fetch:body fields when jsonata: expression is invalid syntax', async () => { + const uiProps: JsonObject = { + 'fetch:method': 'POST', + 'fetch:body': { + ok: 'literal-only', + badCompile: 'jsonata:.', + badSlash: 'jsonata:/', + }, + }; + const formData: JsonObject = {}; + + const requestInit = await getRequestInit( + uiProps, + 'fetch', + unitEvaluator, + formData, + ); + expect(JSON.parse(requestInit.body as string)).toEqual({ + ok: 'literal-only', + }); + }); }); diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useRequestInit.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useRequestInit.ts index 9eec19df6c..f9cc167554 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useRequestInit.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/useRequestInit.ts @@ -53,9 +53,16 @@ const evaluateJsonataInValue = async ( if (!expression) { return value; } - const compiled = jsonata(expression); - const evaluated = await compiled.evaluate(formData); - return evaluated === undefined ? UNDEFINED_VALUE : (evaluated as JsonValue); + try { + const compiled = jsonata(expression); + const evaluated = await compiled.evaluate(formData); + return evaluated === undefined + ? UNDEFINED_VALUE + : (evaluated as JsonValue); + } catch { + // Invalid or failing JSONata (e.g. user-typed fragments) must not break fetch body evaluation. + return UNDEFINED_VALUE; + } } if (Array.isArray(value)) { const evaluatedArray = await Promise.all( From 2297bb33bdf5bd652f2d0385a6a09daea4fcd0e1 Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Mon, 11 May 2026 14:10:36 +0530 Subject: [PATCH 3/4] fix(orchestrator-form-widgets): harden fetch selectors against partial input Coerce or empty non-string template results in evaluateFetchResponseSelectorTemplate, treat non-string JSONata results as empty in lenient applySelector paths, and guard SchemaUpdater object extraction. Update tests and changeset. Co-authored-by: Cursor --- .../active-textinput-missing-selector.md | 2 + .../src/utils/applySelector.test.ts | 13 ++++-- .../src/utils/applySelector.ts | 8 +++- .../src/utils/evaluateTemplate.test.ts | 44 +++++++++++++++++++ .../src/utils/evaluateTemplate.ts | 24 +++++++--- .../src/widgets/SchemaUpdater.tsx | 17 +++++-- 6 files changed, 93 insertions(+), 15 deletions(-) diff --git a/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md b/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md index 34bdc14892..9427390b4d 100644 --- a/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md +++ b/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md @@ -5,3 +5,5 @@ Avoid hard failures in ActiveTextInput when `fetch:response:value` points to a missing response property. If JSONata resolves to `undefined` or `null`, treat it as an empty string so retriggered dynamic selector edits do not break the form UI. Wrap JSONata compile and evaluation errors for fetch response selectors: invalid or partial expressions (e.g. lone `.` or `/` while typing) return empty string or empty options where lenient modes apply, and strict callers get clearer `Invalid JSONata` / evaluation error messages. `jsonata:` values in `fetch:body` / `validate:body` that fail to compile or evaluate are skipped instead of breaking the whole request init. + +Lenient fetch selectors also treat non-string JSONata results (e.g. numeric literals) as empty. `evaluateFetchResponseSelectorTemplate` stringifies numeric/boolean form values, returns empty for unusable template results, and swallows template parse errors so symbols like `$` or partial input do not crash the widget. diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts index d2a5705b38..1866b8bb34 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.test.ts @@ -284,11 +284,9 @@ describe('applySelectorString', () => { ); }); - it('still throws for non-string non-nullish when emptyStringWhenMissing is true', async () => { + it('returns empty string when JSONata yields a number and emptyStringWhenMissing is true', async () => { const withNum: JsonObject = { n: 42 }; - await expect(applySelectorString(withNum, 'n', true)).rejects.toThrow( - 'expected string type', - ); + await expect(applySelectorString(withNum, 'n', true)).resolves.toBe(''); }); it('returns empty string for invalid JSONata syntax when emptyStringWhenMissing is true', async () => { @@ -324,6 +322,13 @@ describe('applySelectorArray invalid JSONata', () => { 'Invalid JSONata', ); }); + + it('returns empty array when JSONata yields a number and emptyArrayIfNeeded is true', async () => { + const withNum: JsonObject = { n: 7 }; + await expect( + applySelectorArray(withNum, 'n', true, true), + ).resolves.toStrictEqual([]); + }); }); describe('applySelectorArray - complex queries', () => { diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts index 99289f7eb6..b6769cb224 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/applySelector.ts @@ -70,6 +70,11 @@ export const applySelectorArray = async ( return [...value]; } + if (emptyArrayIfNeeded) { + // Dynamic selectors while typing may yield numbers, objects, or mixed arrays. + return []; + } + throw new Error( `Unexpected result of "${selector}" selector, expected string[] type. Value ${JSON.stringify(value)}`, ); @@ -106,7 +111,8 @@ export const applySelectorString = async ( return value; } - if (emptyStringWhenMissing && (value === undefined || value === null)) { + if (emptyStringWhenMissing) { + // Missing path, numbers/booleans from literals (e.g. selector "1"), objects, etc. return ''; } diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/evaluateTemplate.test.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/evaluateTemplate.test.ts index 754e1de0ff..cb5f75d45c 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/evaluateTemplate.test.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/evaluateTemplate.test.ts @@ -278,4 +278,48 @@ describe('evaluate template', () => { }), ).resolves.toBe("a in $split(a,b, ',') ? 'update' : 'create'"); }); + + it('evaluateFetchResponseSelectorTemplate stringifies numeric template results for JSONata', async () => { + await expect( + evaluateFetchResponseSelectorTemplate({ + unitEvaluator: unitEvaluatorAsInWidgets, + key: 'fetch:response:value', + formData: { step: { sel: 1 } }, + template: '$${{current.step.sel}}', + }), + ).resolves.toBe('1'); + }); + + it('evaluateFetchResponseSelectorTemplate returns empty string for null template expansion', async () => { + await expect( + evaluateFetchResponseSelectorTemplate({ + unitEvaluator: async () => null, + key: 'fetch:response:value', + formData: {}, + template: '$${{x}}', + }), + ).resolves.toBe(''); + }); + + it('evaluateFetchResponseSelectorTemplate returns empty string for solo object expansion', async () => { + await expect( + evaluateFetchResponseSelectorTemplate({ + unitEvaluator: async () => ({ a: 1 }), + key: 'fetch:response:value', + formData: {}, + template: '$${{x}}', + }), + ).resolves.toBe(''); + }); + + it('evaluateFetchResponseSelectorTemplate returns empty string on malformed template', async () => { + await expect( + evaluateFetchResponseSelectorTemplate({ + unitEvaluator: unitEvaluatorAsInWidgets, + key: 'fetch:response:value', + formData: {}, + template: '$${{foo', + }), + ).resolves.toBe(''); + }); }); diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/evaluateTemplate.ts b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/evaluateTemplate.ts index cb897116be..de0c51ecf7 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/evaluateTemplate.ts +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/utils/evaluateTemplate.ts @@ -133,13 +133,25 @@ export const evaluateTemplateString = async ( export const evaluateFetchResponseSelectorTemplate = async ( props: evaluateTemplateStringProps, ): Promise => { - const evaluated = await evaluateTemplateString(props); - if (typeof evaluated !== 'string') { - throw new Error( - `Template evaluation for "${props.key}" must produce a string (JSONata expression), got ${typeof evaluated}`, - ); + let evaluated: JsonValue; + try { + evaluated = await evaluateTemplateString(props); + } catch { + // Malformed `$${{…}}` or evaluator errors while editing should not break fetch widgets. + return ''; } - return evaluated; + if (typeof evaluated === 'string') { + return evaluated; + } + if (evaluated === undefined || evaluated === null) { + return ''; + } + if (typeof evaluated === 'number' || typeof evaluated === 'boolean') { + // Form fields may be numeric/boolean while the selector must be JSONata text. + return String(evaluated); + } + // Solo object/array from template expansion is not a usable JSONata selector string. + return ''; }; export const evaluateTemplate = async ( diff --git a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx index ba5895d366..64f71d1497 100644 --- a/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx +++ b/workspaces/orchestrator/plugins/orchestrator-form-widgets/src/widgets/SchemaUpdater.tsx @@ -95,10 +95,19 @@ export const SchemaUpdater: Widget< responseData: data, uiProps, }); - typedData = (await applySelectorObject( - data, - resolvedSelector, - )) as unknown as SchemaChunksResponse; + if (resolvedSelector.trim()) { + try { + typedData = (await applySelectorObject( + data, + resolvedSelector, + )) as unknown as SchemaChunksResponse; + } catch (reason) { + setLocalError( + reason instanceof Error ? reason.message : String(reason), + ); + return; + } + } } // validate received response before updating From acf8bd463b2cfc87a9804ffbb43f48dd8cdceeae Mon Sep 17 00:00:00 2001 From: Lokananda Prabhu Date: Mon, 11 May 2026 14:12:57 +0530 Subject: [PATCH 4/4] chore(orchestrator): shorten active-textinput changeset copy Co-authored-by: Cursor --- .../.changeset/active-textinput-missing-selector.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md b/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md index 9427390b4d..cba63efe43 100644 --- a/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md +++ b/workspaces/orchestrator/.changeset/active-textinput-missing-selector.md @@ -2,8 +2,4 @@ '@red-hat-developer-hub/backstage-plugin-orchestrator-form-widgets': patch --- -Avoid hard failures in ActiveTextInput when `fetch:response:value` points to a missing response property. If JSONata resolves to `undefined` or `null`, treat it as an empty string so retriggered dynamic selector edits do not break the form UI. - -Wrap JSONata compile and evaluation errors for fetch response selectors: invalid or partial expressions (e.g. lone `.` or `/` while typing) return empty string or empty options where lenient modes apply, and strict callers get clearer `Invalid JSONata` / evaluation error messages. `jsonata:` values in `fetch:body` / `validate:body` that fail to compile or evaluate are skipped instead of breaking the whole request init. - -Lenient fetch selectors also treat non-string JSONata results (e.g. numeric literals) as empty. `evaluateFetchResponseSelectorTemplate` stringifies numeric/boolean form values, returns empty for unusable template results, and swallows template parse errors so symbols like `$` or partial input do not crash the widget. +Make fetch-driven form widgets resilient to invalid or partial JSONata and dynamic selector input so the UI no longer crashes while users edit fields.