frontend-app-api: add support for v2 extensions
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,11 @@
|
||||
|
||||
import {
|
||||
AnyExtensionDataMap,
|
||||
AnyExtensionDataRef,
|
||||
AnyExtensionInputMap,
|
||||
ExtensionDataContainer,
|
||||
ExtensionDataRef,
|
||||
ExtensionInput,
|
||||
ResolvedExtensionInputs,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
@@ -29,7 +32,7 @@ type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
function resolveInputData(
|
||||
function resolveInputDataMap(
|
||||
dataMap: AnyExtensionDataMap,
|
||||
attachment: AppNode,
|
||||
inputName: string,
|
||||
@@ -54,35 +57,76 @@ function resolveInputData(
|
||||
});
|
||||
}
|
||||
|
||||
function resolveInputs(
|
||||
function resolveInputDataContainer(
|
||||
extensionData: Array<AnyExtensionDataRef>,
|
||||
attachment: AppNode,
|
||||
inputName: string,
|
||||
): { node: AppNode } & ExtensionDataContainer<AnyExtensionDataRef> {
|
||||
const dataMap = new Map<string, unknown>();
|
||||
|
||||
for (const ref of extensionData) {
|
||||
if (dataMap.has(ref.id)) {
|
||||
throw new Error(`Unexpected duplicate input data '${ref.id}'`);
|
||||
}
|
||||
const value = attachment.instance?.getData(ref);
|
||||
if (value === undefined && !ref.config.optional) {
|
||||
const expected = extensionData
|
||||
.filter(r => !r.config.optional)
|
||||
.map(r => `'${r.id}'`)
|
||||
.join(', ');
|
||||
|
||||
const provided = [...(attachment.instance?.getDataRefs() ?? [])]
|
||||
.map(r => `'${r.id}'`)
|
||||
.join(', ');
|
||||
|
||||
throw new Error(
|
||||
`extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`,
|
||||
);
|
||||
}
|
||||
|
||||
dataMap.set(ref.id, value);
|
||||
}
|
||||
|
||||
return {
|
||||
node: attachment,
|
||||
get(ref) {
|
||||
return dataMap.get(ref.id);
|
||||
},
|
||||
} as { node: AppNode } & ExtensionDataContainer<AnyExtensionDataRef>;
|
||||
}
|
||||
|
||||
function reportUndeclaredAttachments(
|
||||
id: string,
|
||||
inputMap: AnyExtensionInputMap,
|
||||
inputMap: { [name in string]: unknown },
|
||||
attachments: ReadonlyMap<string, AppNode[]>,
|
||||
): ResolvedExtensionInputs<AnyExtensionInputMap> {
|
||||
) {
|
||||
const undeclaredAttachments = Array.from(attachments.entries()).filter(
|
||||
([inputName]) => inputMap[inputName] === undefined,
|
||||
);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const inputNames = Object.keys(inputMap);
|
||||
const inputNames = Object.keys(inputMap);
|
||||
|
||||
for (const [name, nodes] of undeclaredAttachments) {
|
||||
const pl = nodes.length > 1;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
[
|
||||
`The extension${pl ? 's' : ''} '${nodes
|
||||
.map(n => n.spec.id)
|
||||
.join("', '")}' ${pl ? 'are' : 'is'}`,
|
||||
`attached to the input '${name}' of the extension '${id}', but it`,
|
||||
inputNames.length === 0
|
||||
? 'has no inputs'
|
||||
: `has no such input (candidates are '${inputNames.join("', '")}')`,
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
for (const [name, nodes] of undeclaredAttachments) {
|
||||
const pl = nodes.length > 1;
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
[
|
||||
`The extension${pl ? 's' : ''} '${nodes
|
||||
.map(n => n.spec.id)
|
||||
.join("', '")}' ${pl ? 'are' : 'is'}`,
|
||||
`attached to the input '${name}' of the extension '${id}', but it`,
|
||||
inputNames.length === 0
|
||||
? 'has no inputs'
|
||||
: `has no such input (candidates are '${inputNames.join("', '")}')`,
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveV1Inputs(
|
||||
inputMap: AnyExtensionInputMap,
|
||||
attachments: ReadonlyMap<string, AppNode[]>,
|
||||
): ResolvedExtensionInputs<AnyExtensionInputMap> {
|
||||
return mapValues(inputMap, (input, inputName) => {
|
||||
const attachedNodes = attachments.get(inputName) ?? [];
|
||||
|
||||
@@ -104,7 +148,7 @@ function resolveInputs(
|
||||
}
|
||||
return {
|
||||
node: attachedNodes[0],
|
||||
output: resolveInputData(
|
||||
output: resolveInputDataMap(
|
||||
input.extensionData,
|
||||
attachedNodes[0],
|
||||
inputName,
|
||||
@@ -114,11 +158,62 @@ function resolveInputs(
|
||||
|
||||
return attachedNodes.map(attachment => ({
|
||||
node: attachment,
|
||||
output: resolveInputData(input.extensionData, attachment, inputName),
|
||||
output: resolveInputDataMap(input.extensionData, attachment, inputName),
|
||||
}));
|
||||
}) as ResolvedExtensionInputs<AnyExtensionInputMap>;
|
||||
}
|
||||
|
||||
function resolveV2Inputs(
|
||||
inputMap: {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
attachments: ReadonlyMap<string, AppNode[]>,
|
||||
): ResolvedExtensionInputs<{
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
}> {
|
||||
return mapValues(inputMap, (input, inputName) => {
|
||||
const attachedNodes = attachments.get(inputName) ?? [];
|
||||
|
||||
if (input.config.singleton) {
|
||||
if (attachedNodes.length > 1) {
|
||||
const attachedNodeIds = attachedNodes.map(e => e.spec.id);
|
||||
throw Error(
|
||||
`expected ${
|
||||
input.config.optional ? 'at most' : 'exactly'
|
||||
} one '${inputName}' input but received multiple: '${attachedNodeIds.join(
|
||||
"', '",
|
||||
)}'`,
|
||||
);
|
||||
} else if (attachedNodes.length === 0) {
|
||||
if (input.config.optional) {
|
||||
return undefined;
|
||||
}
|
||||
throw Error(`input '${inputName}' is required but was not received`);
|
||||
}
|
||||
return resolveInputDataContainer(
|
||||
input.extensionData,
|
||||
attachedNodes[0],
|
||||
inputName,
|
||||
);
|
||||
}
|
||||
|
||||
return attachedNodes.map(attachment =>
|
||||
resolveInputDataContainer(input.extensionData, attachment, inputName),
|
||||
);
|
||||
}) as ResolvedExtensionInputs<{
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createAppNodeInstance(options: {
|
||||
node: AppNode;
|
||||
@@ -141,24 +236,71 @@ export function createAppNodeInstance(options: {
|
||||
try {
|
||||
const internalExtension = toInternalExtension(extension);
|
||||
|
||||
const namedOutputs = internalExtension.factory({
|
||||
node,
|
||||
config: parsedConfig,
|
||||
inputs: resolveInputs(id, internalExtension.inputs, attachments),
|
||||
});
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
reportUndeclaredAttachments(id, internalExtension.inputs, attachments);
|
||||
}
|
||||
|
||||
for (const [name, output] of Object.entries(namedOutputs)) {
|
||||
const ref = internalExtension.output[name];
|
||||
if (!ref) {
|
||||
throw new Error(`unknown output provided via '${name}'`);
|
||||
if (internalExtension.version === 'v1') {
|
||||
const namedOutputs = internalExtension.factory({
|
||||
node,
|
||||
config: parsedConfig,
|
||||
inputs: resolveV1Inputs(internalExtension.inputs, attachments),
|
||||
});
|
||||
|
||||
for (const [name, output] of Object.entries(namedOutputs)) {
|
||||
const ref = internalExtension.output[name];
|
||||
if (!ref) {
|
||||
throw new Error(`unknown output provided via '${name}'`);
|
||||
}
|
||||
if (extensionData.has(ref.id)) {
|
||||
throw new Error(
|
||||
`duplicate extension data '${ref.id}' received via output '${name}'`,
|
||||
);
|
||||
}
|
||||
extensionData.set(ref.id, output);
|
||||
extensionDataRefs.add(ref);
|
||||
}
|
||||
if (extensionData.has(ref.id)) {
|
||||
} else if (internalExtension.version === 'v2') {
|
||||
const outputDataValues = internalExtension.factory({
|
||||
node,
|
||||
config: parsedConfig,
|
||||
inputs: resolveV2Inputs(internalExtension.inputs, attachments),
|
||||
});
|
||||
|
||||
const outputDataMap = new Map<string, unknown>();
|
||||
for (const value of outputDataValues) {
|
||||
if (outputDataMap.has(value.id)) {
|
||||
throw new Error(`duplicate extension data output '${value.id}'`);
|
||||
}
|
||||
outputDataMap.set(value.id, value.value);
|
||||
}
|
||||
|
||||
for (const ref of internalExtension.output) {
|
||||
const value = outputDataMap.get(ref.id);
|
||||
outputDataMap.delete(ref.id);
|
||||
if (value === undefined) {
|
||||
if (!ref.config.optional) {
|
||||
throw new Error(
|
||||
`missing required extension data output '${ref.id}'`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
extensionData.set(ref.id, value);
|
||||
extensionDataRefs.add(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputDataMap.size > 0) {
|
||||
throw new Error(
|
||||
`duplicate extension data '${ref.id}' received via output '${name}'`,
|
||||
`unexpected output '${Array.from(outputDataMap.keys()).join(
|
||||
"', '",
|
||||
)}'`,
|
||||
);
|
||||
}
|
||||
extensionData.set(ref.id, output);
|
||||
extensionDataRefs.add(ref);
|
||||
} else {
|
||||
throw new Error(
|
||||
`unexpected extension version '${(internalExtension as any).version}'`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
|
||||
Reference in New Issue
Block a user