frontend-app-api: rework app error types
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
```ts
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { AppNode } from '@backstage/frontend-plugin-api';
|
||||
import { AppNodeSpec } from '@backstage/frontend-plugin-api';
|
||||
import { AppTree } from '@backstage/frontend-plugin-api';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { ExtensionFactoryMiddleware } from '@backstage/frontend-plugin-api';
|
||||
@@ -18,16 +17,97 @@ import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { SubRouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export type AppError = {
|
||||
code: string;
|
||||
message: string;
|
||||
context?: {
|
||||
node?: AppNode;
|
||||
spec?: AppNodeSpec;
|
||||
plugin?: FrontendPlugin;
|
||||
extensionId?: string;
|
||||
inputName?: string;
|
||||
dataRefId?: string;
|
||||
export type AppError =
|
||||
keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes
|
||||
? ICode extends any
|
||||
? {
|
||||
code: ICode;
|
||||
message: string;
|
||||
context: AppErrorTypes[ICode]['context'];
|
||||
}
|
||||
: never
|
||||
: never;
|
||||
|
||||
// @public (undocumented)
|
||||
export type AppErrorTypes = {
|
||||
EXTENSION_IGNORED: {
|
||||
context: {
|
||||
plugin: FrontendPlugin;
|
||||
extensionId: string;
|
||||
};
|
||||
};
|
||||
INVALID_EXTENSION_CONFIG_KEY: {
|
||||
context: {
|
||||
extensionId: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_INPUT_REDIRECT_CONFLICT: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
inputName: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_INPUT_DATA_IGNORED: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
inputName: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_INPUT_DATA_MISSING: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
inputName: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_ATTACHMENT_CONFLICT: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
inputName: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_ATTACHMENT_MISSING: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
inputName: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_CONFIGURATION_INVALID: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
};
|
||||
};
|
||||
EXTENSION_INVALID: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
};
|
||||
};
|
||||
EXTENSION_OUTPUT_CONFLICT: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
dataRefId: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_OUTPUT_MISSING: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
dataRefId: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_OUTPUT_IGNORED: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
dataRefId: string;
|
||||
};
|
||||
};
|
||||
EXTENSION_FACTORY_ERROR: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
};
|
||||
};
|
||||
API_EXTENSION_INVALID: {
|
||||
context: {
|
||||
node: AppNode;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1098,7 +1098,7 @@ describe('instantiateAppNodeTree', () => {
|
||||
});
|
||||
const errors = collector.collectErrors();
|
||||
for (const error of errors ?? []) {
|
||||
expect(error.context?.node).toBe(node);
|
||||
expect('node' in error.context && error.context.node).toBe(node);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ function resolveInputDataContainer(
|
||||
extensionData: Array<ExtensionDataRef>,
|
||||
attachment: AppNode,
|
||||
inputName: string,
|
||||
collector: ErrorCollector<'node' | 'inputName'>,
|
||||
collector: ErrorCollector<{ node: AppNode; inputName: string }>,
|
||||
): { node: AppNode } & ExtensionDataContainer<ExtensionDataRef> {
|
||||
const dataMap = new Map<string, unknown>();
|
||||
|
||||
@@ -250,7 +250,7 @@ function resolveV2Inputs(
|
||||
>;
|
||||
},
|
||||
attachments: ReadonlyMap<string, AppNode[]>,
|
||||
parentCollector: ErrorCollector<'node'>,
|
||||
parentCollector: ErrorCollector<{ node: AppNode }>,
|
||||
): ResolvedExtensionInputs<{
|
||||
[inputName in string]: ExtensionInput<
|
||||
ExtensionDataRef,
|
||||
|
||||
@@ -15,63 +15,81 @@
|
||||
*/
|
||||
|
||||
import { AppNode, FrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Expand } from '@backstage/types';
|
||||
|
||||
type AppErrorTypes = {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AppErrorTypes = {
|
||||
// resolveAppNodeSpecs
|
||||
EXTENSION_IGNORED: 'plugin' | 'extensionId';
|
||||
INVALID_EXTENSION_CONFIG_KEY: 'extensionId';
|
||||
// resolveAppTree
|
||||
EXTENSION_INPUT_REDIRECT_CONFLICT: 'node' | 'inputName';
|
||||
// instantiateAppNodeTree
|
||||
EXTENSION_INPUT_DATA_IGNORED: 'node' | 'inputName';
|
||||
EXTENSION_INPUT_DATA_MISSING: 'node' | 'inputName';
|
||||
EXTENSION_ATTACHMENT_CONFLICT: 'node' | 'inputName';
|
||||
EXTENSION_ATTACHMENT_MISSING: 'node' | 'inputName';
|
||||
EXTENSION_CONFIGURATION_INVALID: 'node';
|
||||
EXTENSION_INVALID: 'node';
|
||||
EXTENSION_OUTPUT_CONFLICT: 'node' | 'dataRefId';
|
||||
EXTENSION_OUTPUT_MISSING: 'node' | 'dataRefId';
|
||||
EXTENSION_OUTPUT_IGNORED: 'node' | 'dataRefId';
|
||||
EXTENSION_FACTORY_ERROR: 'node';
|
||||
// createSpecializedApp
|
||||
API_EXTENSION_INVALID: 'node';
|
||||
};
|
||||
|
||||
type AppErrorContext = {
|
||||
node?: AppNode;
|
||||
plugin?: FrontendPlugin;
|
||||
extensionId?: string;
|
||||
dataRefId?: string;
|
||||
inputName?: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type AppError<TCode extends keyof AppErrorTypes = keyof AppErrorTypes> =
|
||||
{
|
||||
code: TCode;
|
||||
message: string;
|
||||
context: Expand<
|
||||
AppErrorContext &
|
||||
Pick<
|
||||
Required<AppErrorContext>,
|
||||
keyof AppErrorTypes extends TCode ? never : AppErrorTypes[TCode]
|
||||
>
|
||||
>;
|
||||
EXTENSION_IGNORED: {
|
||||
context: { plugin: FrontendPlugin; extensionId: string };
|
||||
};
|
||||
INVALID_EXTENSION_CONFIG_KEY: {
|
||||
context: { extensionId: string };
|
||||
};
|
||||
// resolveAppTree
|
||||
EXTENSION_INPUT_REDIRECT_CONFLICT: {
|
||||
context: { node: AppNode; inputName: string };
|
||||
};
|
||||
// instantiateAppNodeTree
|
||||
EXTENSION_INPUT_DATA_IGNORED: {
|
||||
context: { node: AppNode; inputName: string };
|
||||
};
|
||||
EXTENSION_INPUT_DATA_MISSING: {
|
||||
context: { node: AppNode; inputName: string };
|
||||
};
|
||||
EXTENSION_ATTACHMENT_CONFLICT: {
|
||||
context: { node: AppNode; inputName: string };
|
||||
};
|
||||
EXTENSION_ATTACHMENT_MISSING: {
|
||||
context: { node: AppNode; inputName: string };
|
||||
};
|
||||
EXTENSION_CONFIGURATION_INVALID: {
|
||||
context: { node: AppNode };
|
||||
};
|
||||
EXTENSION_INVALID: {
|
||||
context: { node: AppNode };
|
||||
};
|
||||
EXTENSION_OUTPUT_CONFLICT: {
|
||||
context: { node: AppNode; dataRefId: string };
|
||||
};
|
||||
EXTENSION_OUTPUT_MISSING: {
|
||||
context: { node: AppNode; dataRefId: string };
|
||||
};
|
||||
EXTENSION_OUTPUT_IGNORED: {
|
||||
context: { node: AppNode; dataRefId: string };
|
||||
};
|
||||
EXTENSION_FACTORY_ERROR: {
|
||||
context: { node: AppNode };
|
||||
};
|
||||
// createSpecializedApp
|
||||
API_EXTENSION_INVALID: {
|
||||
context: { node: AppNode };
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AppError =
|
||||
keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes
|
||||
? ICode extends any
|
||||
? {
|
||||
code: ICode;
|
||||
message: string;
|
||||
context: AppErrorTypes[ICode]['context'];
|
||||
}
|
||||
: never
|
||||
: never;
|
||||
|
||||
/** @internal */
|
||||
export interface ErrorCollector<
|
||||
TContext extends keyof AppErrorContext = never,
|
||||
> {
|
||||
// Type-only: here to make sure that all required keys are present
|
||||
$$contextKeys: { [K in TContext]: K };
|
||||
export interface ErrorCollector<TContext extends {} = {}> {
|
||||
report<TCode extends keyof AppErrorTypes>(
|
||||
report: Exclude<
|
||||
AppErrorTypes[TCode],
|
||||
TContext
|
||||
> extends infer IContext extends keyof AppErrorContext
|
||||
? [IContext] extends [never]
|
||||
report: Omit<
|
||||
AppErrorTypes[TCode]['context'],
|
||||
keyof TContext
|
||||
> extends infer IContext extends {}
|
||||
? [{}] extends [IContext]
|
||||
? {
|
||||
code: TCode;
|
||||
message: string;
|
||||
@@ -79,28 +97,28 @@ export interface ErrorCollector<
|
||||
: {
|
||||
code: TCode;
|
||||
message: string;
|
||||
context: Pick<Required<AppErrorContext>, IContext>;
|
||||
context: IContext;
|
||||
}
|
||||
: never,
|
||||
): void;
|
||||
child<TAdditionalContext extends AppErrorContext>(
|
||||
context?: TAdditionalContext & AppErrorContext,
|
||||
): ErrorCollector<
|
||||
(TContext | keyof TAdditionalContext) & keyof AppErrorContext
|
||||
>;
|
||||
child<TAdditionalContext extends {}>(
|
||||
context: TAdditionalContext,
|
||||
): ErrorCollector<TContext & TAdditionalContext>;
|
||||
collectErrors(): AppError[] | undefined;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createErrorCollector(
|
||||
context?: AppError['context'],
|
||||
context?: Partial<AppError['context']>,
|
||||
): ErrorCollector {
|
||||
const errors: AppError[] = [];
|
||||
const children: ErrorCollector[] = [];
|
||||
return {
|
||||
$$contextKeys: null as any,
|
||||
report(report) {
|
||||
errors.push({ ...report, context: { ...context, ...report.context } });
|
||||
report(report: { code: string; message: string; context?: {} }) {
|
||||
errors.push({
|
||||
...report,
|
||||
context: { ...context, ...report.context },
|
||||
} as AppError);
|
||||
},
|
||||
collectErrors() {
|
||||
const allErrors = [
|
||||
|
||||
@@ -19,4 +19,4 @@ export {
|
||||
type CreateSpecializedAppOptions,
|
||||
} from './createSpecializedApp';
|
||||
export { type FrontendPluginInfoResolver } from './createPluginInfoAttacher';
|
||||
export { type AppError } from './createErrorCollector';
|
||||
export { type AppError, type AppErrorTypes } from './createErrorCollector';
|
||||
|
||||
Reference in New Issue
Block a user