frontend-app-api: attribute attachemnt data failures to the attached extension
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Updated error reporting and app tree resolution logic to attribute errors to the correct extension and allow app startup to proceed more optimistically:
|
||||
|
||||
- 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.
|
||||
@@ -1574,41 +1574,109 @@ describe('instantiateAppNodeTree', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with multiple inputs that did not provide required data', () => {
|
||||
const node = makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput([otherDataRef], {
|
||||
singleton: true,
|
||||
}),
|
||||
},
|
||||
output: [],
|
||||
factory: () => [],
|
||||
}),
|
||||
{ namespace: 'app' },
|
||||
),
|
||||
describe('with attachment failures', () => {
|
||||
const inputCountRef = createExtensionDataRef<number>().with({
|
||||
id: 'input-count',
|
||||
});
|
||||
|
||||
const attachmentWithoutRequiredData = makeInstanceWithId(
|
||||
simpleExtension,
|
||||
undefined,
|
||||
);
|
||||
expect(
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
|
||||
]),
|
||||
node,
|
||||
collector,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
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 'singleton' requires ('other')",
|
||||
context: { node, inputName: 'singleton' },
|
||||
},
|
||||
]);
|
||||
|
||||
it('should proceed if input is optional', () => {
|
||||
const node = makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput([otherDataRef], {
|
||||
singleton: true,
|
||||
optional: true,
|
||||
}),
|
||||
},
|
||||
output: [inputCountRef],
|
||||
factory: ({ inputs }) => [
|
||||
inputCountRef(inputs.singleton ? 1 : 0),
|
||||
],
|
||||
}),
|
||||
{ namespace: 'app' },
|
||||
),
|
||||
);
|
||||
expect(
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
['singleton', [attachmentWithoutRequiredData]],
|
||||
]),
|
||||
node,
|
||||
collector,
|
||||
})?.getData(inputCountRef),
|
||||
).toBe(0);
|
||||
|
||||
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 'singleton' requires ('other')",
|
||||
context: {
|
||||
node: attachmentWithoutRequiredData,
|
||||
inputName: 'singleton',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fail if input is required', () => {
|
||||
const node = makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput([otherDataRef], {
|
||||
singleton: true,
|
||||
}),
|
||||
},
|
||||
output: [inputCountRef],
|
||||
factory: ({ inputs }) => [
|
||||
inputCountRef(inputs.singleton ? 1 : 0),
|
||||
],
|
||||
}),
|
||||
{ namespace: 'app' },
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
createAppNodeInstance({
|
||||
apis: testApis,
|
||||
attachments: new Map([
|
||||
['singleton', [attachmentWithoutRequiredData]],
|
||||
]),
|
||||
node,
|
||||
collector,
|
||||
})?.getData(inputCountRef),
|
||||
).toBeUndefined();
|
||||
|
||||
expect(collector.collectErrors()).toEqual([
|
||||
{
|
||||
code: 'EXTENSION_ATTACHMENT_MISSING',
|
||||
message:
|
||||
"input 'singleton' is required but it failed to be instantiated",
|
||||
context: { inputName: 'singleton', node },
|
||||
},
|
||||
{
|
||||
code: 'EXTENSION_INPUT_DATA_MISSING',
|
||||
message:
|
||||
"extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')",
|
||||
context: {
|
||||
node: attachmentWithoutRequiredData,
|
||||
inputName: 'singleton',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,7 +119,7 @@ function resolveInputDataContainer(
|
||||
.map(r => `'${r.id}'`)
|
||||
.join(', ');
|
||||
|
||||
collector.report({
|
||||
collector.child({ node: attachment }).report({
|
||||
code: 'EXTENSION_INPUT_DATA_MISSING',
|
||||
message: `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`,
|
||||
});
|
||||
@@ -271,12 +271,25 @@ function resolveV2Inputs(
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return resolveInputDataContainer(
|
||||
input.extensionData,
|
||||
attachedNodes[0],
|
||||
inputName,
|
||||
collector,
|
||||
);
|
||||
try {
|
||||
return resolveInputDataContainer(
|
||||
input.extensionData,
|
||||
attachedNodes[0],
|
||||
inputName,
|
||||
collector,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error === INSTANTIATION_FAILED) {
|
||||
if (input.config.optional) {
|
||||
return undefined;
|
||||
}
|
||||
collector.report({
|
||||
code: 'EXTENSION_ATTACHMENT_MISSING',
|
||||
message: `input '${inputName}' is required but it failed to be instantiated`,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return mapWithFailures(attachedNodes, attachment =>
|
||||
|
||||
Reference in New Issue
Block a user