From efea76beb0df3e23ecdac6fc8b3e5b1d3c5067f7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 23 Jan 2026 13:52:47 +0100 Subject: [PATCH] frontend-app-api: filter out failed attachments Signed-off-by: Patrik Oldsberg --- .changeset/good-eggs-tell.md | 1 + .../src/tree/instantiateAppNodeTree.test.ts | 49 +++++++++++++++++++ .../src/tree/instantiateAppNodeTree.ts | 28 ++++++----- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/.changeset/good-eggs-tell.md b/.changeset/good-eggs-tell.md index 86767c24a4..04ce63db7d 100644 --- a/.changeset/good-eggs-tell.md +++ b/.changeset/good-eggs-tell.md @@ -6,3 +6,4 @@ Updated error reporting and app tree resolution logic to attribute errors to the - If an attachment fails to provide the required input data, the error is now attributed to the attachment rather than the parent extension. - Singleton extension inputs will now only forward attachment errors if the input is required. +- Array extension inputs will now filter out failed attachments instead of failing the entire app tree resolution. diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index d15873f5d1..485e9d95bd 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -1677,6 +1677,55 @@ describe('instantiateAppNodeTree', () => { }, ]); }); + + it('should filter out failed attachments for non-singleton inputs', () => { + const node = makeNode( + resolveExtensionDefinition( + createExtension({ + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + children: createExtensionInput([otherDataRef]), + }, + output: [inputCountRef], + factory: ({ inputs }) => [ + inputCountRef(inputs.children.length), + ], + }), + { namespace: 'app' }, + ), + ); + expect( + createAppNodeInstance({ + apis: testApis, + attachments: new Map([ + [ + 'children', + [ + attachmentWithoutRequiredData, + makeInstanceWithId(simpleExtension, { + other: 42, + }), + ], + ], + ]), + node, + collector, + })?.getData(inputCountRef), + ).toBe(1); + + expect(collector.collectErrors()).toEqual([ + { + code: 'EXTENSION_INPUT_DATA_MISSING', + message: + "extension 'app/test' could not be attached because its output data ('test') does not match what the input 'children' requires ('other')", + context: { + node: attachmentWithoutRequiredData, + inputName: 'children', + }, + }, + ]); + }); }); }); }); diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 55bea60274..ca0d785fee 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -38,21 +38,22 @@ const INSTANTIATION_FAILED = new Error('Instantiation failed'); function mapWithFailures( iterable: Iterable, callback: (item: T) => U, + options?: { ignoreFailures?: boolean }, ): U[] { let failed = false; - const results = Array.from(iterable).map(item => { + const results = []; + for (const item of iterable) { try { - return callback(item); + results.push(callback(item)); } catch (error) { if (error === INSTANTIATION_FAILED) { failed = true; } else { throw error; } - return null as any; } - }); - if (failed) { + } + if (failed && !options?.ignoreFailures) { throw INSTANTIATION_FAILED; } return results; @@ -292,13 +293,16 @@ function resolveV2Inputs( } } - return mapWithFailures(attachedNodes, attachment => - resolveInputDataContainer( - input.extensionData, - attachment, - inputName, - collector, - ), + return mapWithFailures( + attachedNodes, + attachment => + resolveInputDataContainer( + input.extensionData, + attachment, + inputName, + collector, + ), + { ignoreFailures: true }, ); }) as ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput }>; }