frontend-*-api: better error when factories return invalid data
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Improved runtime error message clarity when extension factories don't return an iterable object.
|
||||
@@ -18,10 +18,13 @@ import {
|
||||
AppNode,
|
||||
Extension,
|
||||
ExtensionDataRef,
|
||||
ExtensionDefinition,
|
||||
ExtensionFactoryMiddleware,
|
||||
ExtensionInput,
|
||||
PortableSchema,
|
||||
ResolvedExtensionInput,
|
||||
createExtension,
|
||||
createExtensionBlueprint,
|
||||
createExtensionDataRef,
|
||||
createExtensionInput,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
@@ -957,6 +960,127 @@ describe('instantiateAppNodeTree', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if extension factories do not provide an iterable object', () => {
|
||||
function createInstance(
|
||||
extension: ExtensionDefinition,
|
||||
middleware?: ExtensionFactoryMiddleware,
|
||||
) {
|
||||
return createAppNodeInstance({
|
||||
extensionFactoryMiddleware: middleware,
|
||||
apis: testApis,
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(extension, { namespace: 'test' }),
|
||||
),
|
||||
attachments: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
const baseOpts = {
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: [testDataRef],
|
||||
};
|
||||
|
||||
const badFactory = () => 'not-iterable' as any;
|
||||
const goodFactory = () => [testDataRef('test')];
|
||||
|
||||
expect(() =>
|
||||
createInstance(
|
||||
createExtension({
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: [testDataRef],
|
||||
factory: badFactory,
|
||||
}),
|
||||
),
|
||||
).toThrow(
|
||||
`Failed to instantiate extension 'test', extension factory did not provide an iterable object`,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
createInstance(
|
||||
createExtension({
|
||||
...baseOpts,
|
||||
factory: goodFactory,
|
||||
}).override({
|
||||
factory: badFactory,
|
||||
}),
|
||||
),
|
||||
).toThrow(
|
||||
`Failed to instantiate extension 'test', extension factory override did not provide an iterable object`,
|
||||
);
|
||||
|
||||
// Bad middleware
|
||||
expect(() =>
|
||||
createInstance(
|
||||
createExtension({
|
||||
...baseOpts,
|
||||
factory: goodFactory,
|
||||
}),
|
||||
() => 'not-iterable' as any,
|
||||
),
|
||||
).toThrow(
|
||||
`Failed to instantiate extension 'test', extension factory middleware did not provide an iterable object`,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
createInstance(
|
||||
createExtensionBlueprint({
|
||||
kind: 'test',
|
||||
...baseOpts,
|
||||
factory: badFactory,
|
||||
}).make({ params: {} }),
|
||||
),
|
||||
).toThrow(
|
||||
`Failed to instantiate extension 'test:test', extension factory did not provide an iterable object`,
|
||||
);
|
||||
|
||||
// Using makeWithOverrides
|
||||
expect(() =>
|
||||
createInstance(
|
||||
createExtensionBlueprint({
|
||||
kind: 'test',
|
||||
...baseOpts,
|
||||
factory: goodFactory,
|
||||
}).makeWithOverrides({
|
||||
factory: badFactory,
|
||||
}),
|
||||
),
|
||||
).toThrow(
|
||||
`Failed to instantiate extension 'test:test', extension factory did not provide an iterable object`,
|
||||
);
|
||||
|
||||
// Using makeWithOverrides and factory middleware
|
||||
expect(() =>
|
||||
createInstance(
|
||||
createExtensionBlueprint({
|
||||
kind: 'test',
|
||||
...baseOpts,
|
||||
factory: goodFactory,
|
||||
}).makeWithOverrides({
|
||||
factory: badFactory,
|
||||
}),
|
||||
orig => orig(),
|
||||
),
|
||||
).toThrow(
|
||||
`Failed to instantiate extension 'test:test', extension factory did not provide an iterable object`,
|
||||
);
|
||||
|
||||
// Using makeWithOverrides and factory middleware
|
||||
expect(() =>
|
||||
createInstance(
|
||||
createExtensionBlueprint({
|
||||
kind: 'test',
|
||||
...baseOpts,
|
||||
factory: badFactory,
|
||||
}).makeWithOverrides({
|
||||
factory: orig => orig({ params: {} }),
|
||||
}),
|
||||
orig => orig(),
|
||||
),
|
||||
).toThrow(
|
||||
`Failed to instantiate extension 'test:test', original blueprint factory did not provide an iterable object`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should forward extension factory errors', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
|
||||
@@ -309,11 +309,20 @@ export function createAppNodeInstance(options: {
|
||||
inputs: context.inputs,
|
||||
config: overrideContext?.config ?? context.config,
|
||||
}),
|
||||
'extension factory',
|
||||
);
|
||||
}, context),
|
||||
'extension factory middleware',
|
||||
)
|
||||
: internalExtension.factory(context);
|
||||
|
||||
if (
|
||||
typeof outputDataValues !== 'object' ||
|
||||
!outputDataValues?.[Symbol.iterator]
|
||||
) {
|
||||
throw new Error('extension factory did not provide an iterable object');
|
||||
}
|
||||
|
||||
const outputDataMap = new Map<string, unknown>();
|
||||
for (const value of outputDataValues) {
|
||||
if (outputDataMap.has(value.id)) {
|
||||
|
||||
@@ -373,6 +373,7 @@ function mergeExtensionFactoryMiddleware(
|
||||
apis: ctx.apis,
|
||||
config: ctxOverrides?.config ?? ctx.config,
|
||||
}),
|
||||
'extension factory middleware',
|
||||
);
|
||||
}, ctx);
|
||||
};
|
||||
|
||||
@@ -26,8 +26,13 @@ export function createExtensionDataContainer<UData extends ExtensionDataRef>(
|
||||
? ExtensionDataValue<IData, IId>
|
||||
: never
|
||||
>,
|
||||
contextName: string,
|
||||
declaredRefs?: ExtensionDataRef<any, any, any>[],
|
||||
): ExtensionDataContainer<UData> {
|
||||
if (typeof values !== 'object' || !values?.[Symbol.iterator]) {
|
||||
throw new Error(`${contextName} did not provide an iterable object`);
|
||||
}
|
||||
|
||||
const container = new Map<string, ExtensionDataValue<any, any>>();
|
||||
const verifyRefs =
|
||||
declaredRefs && new Map(declaredRefs.map(ref => [ref.id, ref]));
|
||||
|
||||
@@ -448,6 +448,7 @@ export function createExtension<
|
||||
) as any,
|
||||
[ctxParamsSymbol as any]: innerContext?.params,
|
||||
}) as Iterable<any>,
|
||||
'original extension factory',
|
||||
options.output,
|
||||
);
|
||||
},
|
||||
@@ -459,6 +460,15 @@ export function createExtension<
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
typeof parentResult !== 'object' ||
|
||||
!parentResult?.[Symbol.iterator]
|
||||
) {
|
||||
throw new Error(
|
||||
'extension factory override did not provide an iterable object',
|
||||
);
|
||||
}
|
||||
|
||||
const deduplicatedResult = new Map<
|
||||
string,
|
||||
ExtensionDataValue<any, any>
|
||||
|
||||
@@ -391,7 +391,7 @@ describe('createExtensionBlueprint', () => {
|
||||
});
|
||||
|
||||
const mockInput = (node: string, ...data: ExtensionDataValue<any, any>[]) =>
|
||||
Object.assign(createExtensionDataContainer(data), {
|
||||
Object.assign(createExtensionDataContainer(data, 'mock'), {
|
||||
node,
|
||||
});
|
||||
const mockParentInputs = {
|
||||
|
||||
@@ -521,6 +521,7 @@ export function createExtensionBlueprint<
|
||||
) as any,
|
||||
},
|
||||
) as Iterable<any>,
|
||||
'original blueprint factory',
|
||||
options.output,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -119,6 +119,7 @@ export function resolveInputOverrides(
|
||||
if (providedData) {
|
||||
const providedContainer = createExtensionDataContainer(
|
||||
providedData as Iterable<ExtensionDataValue<any, any>>,
|
||||
'extension input override',
|
||||
declaredInput.extensionData,
|
||||
);
|
||||
if (!originalInput) {
|
||||
@@ -157,6 +158,7 @@ export function resolveInputOverrides(
|
||||
newInputs[name] = providedData.map((data, i) => {
|
||||
const providedContainer = createExtensionDataContainer(
|
||||
data as Iterable<ExtensionDataValue<any, any>>,
|
||||
'extension input override',
|
||||
declaredInput.extensionData,
|
||||
);
|
||||
return Object.assign(providedContainer, {
|
||||
|
||||
Reference in New Issue
Block a user