Introduce the @backstage/errors package.
Encode thrown errors in the backend as a JSON payload using a facility in that package, and render helpful frontend displays of those errors. Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-client': patch
|
||||
---
|
||||
|
||||
Throw the new `ServerResponseError` from `@backstage/errors`
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
'@backstage/backend-common': minor
|
||||
---
|
||||
|
||||
Encode thrown errors in the backend as a JSON payload. This is technically a breaking change, since the response format even of errors are part of the contract. If you relied on the response being text, you will now have some extra JSON "noise" in it. It should still be readable by end users though.
|
||||
|
||||
Before:
|
||||
|
||||
```
|
||||
NotFoundError: No entity named 'tara.macgovern2' found, with kind 'user' in namespace 'default'
|
||||
at eval (webpack-internal:///../../plugins/catalog-backend/src/service/router.ts:117:17)
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"statusCode": 404,
|
||||
"name": "NotFoundError",
|
||||
"message": "No entity named 'tara.macgovern2' found, with kind 'user' in namespace 'default'",
|
||||
"stack": "NotFoundError: No entity named 'tara.macgovern2' found, with kind 'user' in namespace 'default'\n at eval (webpack-internal:///../../plugins/catalog-backend/src/service/router.ts:117:17)"
|
||||
},
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "/entities/by-name/user/default/tara.macgovern2"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Add a `ErrorResponsePanel` to render `ServerResponseError` from `@backstage/errors`
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
---
|
||||
|
||||
Use errors from `@backstage/errors`
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/backend-common': minor
|
||||
---
|
||||
|
||||
Removed the custom error types (e.g. `NotFoundError`). Those are now instead in the new `@backstage/errors` package. This is a breaking change, and you will have to update your imports if you were using these types.
|
||||
|
||||
```diff
|
||||
-import { NotFoundError } from '@backstage/backend-common';
|
||||
+import { NotFoundError } from '@backstage/errors';
|
||||
```
|
||||
@@ -32,6 +32,7 @@
|
||||
"@backstage/cli-common": "^0.1.1",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/config-loader": "^0.5.1",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/integration": "^0.5.1",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"@types/cors": "^2.8.6",
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
export * from './config';
|
||||
export * from './database';
|
||||
export * from './discovery';
|
||||
export * from './errors';
|
||||
export * from './hot';
|
||||
export * from './logging';
|
||||
export * from './middleware';
|
||||
|
||||
@@ -14,10 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthenticationError,
|
||||
ConflictError,
|
||||
InputError,
|
||||
NotAllowedError,
|
||||
NotFoundError,
|
||||
NotModifiedError,
|
||||
} from '@backstage/errors';
|
||||
import express from 'express';
|
||||
import createError from 'http-errors';
|
||||
import request from 'supertest';
|
||||
import * as errors from '../errors';
|
||||
import { errorHandler } from './errorHandler';
|
||||
|
||||
describe('errorHandler', () => {
|
||||
@@ -31,17 +38,27 @@ describe('errorHandler', () => {
|
||||
const response = await request(app).get('/breaks');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
expect(response.text).toBe('some message');
|
||||
expect(response.body).toEqual({
|
||||
error: {
|
||||
statusCode: 500,
|
||||
name: 'Error',
|
||||
message: 'some message',
|
||||
},
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/breaks',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('doesnt try to send the response again if its already been sent', async () => {
|
||||
it('does not try to send the response again if its already been sent', async () => {
|
||||
const app = express();
|
||||
const mockSend = jest.fn();
|
||||
|
||||
app.use('/works_with_async_fail', (_, res) => {
|
||||
res.status(200).send('hello');
|
||||
|
||||
// mutate the response object to test the middlware.
|
||||
// mutate the response object to test the middleware.
|
||||
// it's hard to catch errors inside middleware from the outside.
|
||||
// @ts-ignore
|
||||
res.send = mockSend;
|
||||
@@ -67,38 +84,61 @@ describe('errorHandler', () => {
|
||||
const response = await request(app).get('/breaks');
|
||||
|
||||
expect(response.status).toBe(432);
|
||||
expect(response.text).toContain('Some Message');
|
||||
expect(response.body).toEqual({
|
||||
error: {
|
||||
statusCode: 432,
|
||||
name: 'BadRequestError',
|
||||
message: 'Some Message',
|
||||
},
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/breaks',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles well-known error classes', async () => {
|
||||
const app = express();
|
||||
app.use('/NotModifiedError', () => {
|
||||
throw new errors.NotModifiedError();
|
||||
throw new NotModifiedError();
|
||||
});
|
||||
app.use('/InputError', () => {
|
||||
throw new errors.InputError();
|
||||
throw new InputError();
|
||||
});
|
||||
app.use('/AuthenticationError', () => {
|
||||
throw new errors.AuthenticationError();
|
||||
throw new AuthenticationError();
|
||||
});
|
||||
app.use('/NotAllowedError', () => {
|
||||
throw new errors.NotAllowedError();
|
||||
throw new NotAllowedError();
|
||||
});
|
||||
app.use('/NotFoundError', () => {
|
||||
throw new errors.NotFoundError();
|
||||
throw new NotFoundError();
|
||||
});
|
||||
app.use('/ConflictError', () => {
|
||||
throw new errors.ConflictError();
|
||||
throw new ConflictError();
|
||||
});
|
||||
app.use(errorHandler());
|
||||
|
||||
const r = request(app);
|
||||
expect((await r.get('/NotModifiedError')).status).toBe(304);
|
||||
expect((await r.get('/InputError')).status).toBe(400);
|
||||
expect((await r.get('/InputError')).body.error.name).toBe('InputError');
|
||||
expect((await r.get('/AuthenticationError')).status).toBe(401);
|
||||
expect((await r.get('/AuthenticationError')).body.error.name).toBe(
|
||||
'AuthenticationError',
|
||||
);
|
||||
expect((await r.get('/NotAllowedError')).status).toBe(403);
|
||||
expect((await r.get('/NotAllowedError')).body.error.name).toBe(
|
||||
'NotAllowedError',
|
||||
);
|
||||
expect((await r.get('/NotFoundError')).status).toBe(404);
|
||||
expect((await r.get('/NotFoundError')).body.error.name).toBe(
|
||||
'NotFoundError',
|
||||
);
|
||||
expect((await r.get('/ConflictError')).status).toBe(409);
|
||||
expect((await r.get('/ConflictError')).body.error.name).toBe(
|
||||
'ConflictError',
|
||||
);
|
||||
});
|
||||
|
||||
it('logs all 500 errors', async () => {
|
||||
@@ -126,7 +166,7 @@ describe('errorHandler', () => {
|
||||
mockLogger.child.mockImplementation(() => mockLogger as any);
|
||||
|
||||
app.use('/NotFound', () => {
|
||||
throw new errors.NotFoundError();
|
||||
throw new NotFoundError();
|
||||
});
|
||||
app.use(errorHandler({ logger: mockLogger as any }));
|
||||
|
||||
@@ -142,7 +182,7 @@ describe('errorHandler', () => {
|
||||
mockLogger.child.mockImplementation(() => mockLogger as any);
|
||||
|
||||
app.use('/NotFound', () => {
|
||||
throw new errors.NotFoundError();
|
||||
throw new NotFoundError();
|
||||
});
|
||||
app.use(errorHandler({ logger: mockLogger as any, logClientErrors: true }));
|
||||
|
||||
|
||||
@@ -16,7 +16,15 @@
|
||||
|
||||
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import * as errors from '../errors';
|
||||
import {
|
||||
NotModifiedError,
|
||||
InputError,
|
||||
AuthenticationError,
|
||||
NotAllowedError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
ServerResponseErrorBody,
|
||||
} from '@backstage/errors';
|
||||
import { getRootLogger } from '../logging';
|
||||
|
||||
export type ErrorHandlerOptions = {
|
||||
@@ -48,14 +56,13 @@ export type ErrorHandlerOptions = {
|
||||
* This is commonly the very last middleware in the chain.
|
||||
*
|
||||
* Its primary purpose is not to do translation of business logic exceptions,
|
||||
* but rather to be a gobal catch-all for uncaught "fatal" errors that are
|
||||
* but rather to be a global catch-all for uncaught "fatal" errors that are
|
||||
* expected to result in a 500 error. However, it also does handle some common
|
||||
* error types (such as http-error exceptions) and returns the enclosed status
|
||||
* code accordingly.
|
||||
*
|
||||
* @returns An Express error request handler
|
||||
*/
|
||||
|
||||
export function errorHandler(
|
||||
options: ErrorHandlerOptions = {},
|
||||
): ErrorRequestHandler {
|
||||
@@ -66,12 +73,12 @@ export function errorHandler(
|
||||
type: 'errorHandler',
|
||||
});
|
||||
|
||||
return (
|
||||
error: Error,
|
||||
_request: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
return (error: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
const statusCode = getStatusCode(error);
|
||||
if (options.logClientErrors || statusCode >= 500) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
if (res.headersSent) {
|
||||
// If the headers have already been sent, do not send the response again
|
||||
// as this will throw an error in the backend.
|
||||
@@ -79,16 +86,20 @@ export function errorHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
const status = getStatusCode(error);
|
||||
const message = showStackTraces ? error.stack : error.message;
|
||||
const body: ServerResponseErrorBody = {
|
||||
error: {
|
||||
statusCode,
|
||||
name: error.name || 'Error',
|
||||
message: error.message || '<no reason given>',
|
||||
stack: showStackTraces ? error.stack : undefined,
|
||||
},
|
||||
request: {
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
},
|
||||
};
|
||||
|
||||
if (options.logClientErrors || status >= 500) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
res.status(status);
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.send(message);
|
||||
res.status(statusCode).json(body);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,17 +120,17 @@ function getStatusCode(error: Error): number {
|
||||
|
||||
// Handle well-known error types
|
||||
switch (error.name) {
|
||||
case errors.NotModifiedError.name:
|
||||
case NotModifiedError.name:
|
||||
return 304;
|
||||
case errors.InputError.name:
|
||||
case InputError.name:
|
||||
return 400;
|
||||
case errors.AuthenticationError.name:
|
||||
case AuthenticationError.name:
|
||||
return 401;
|
||||
case errors.NotAllowedError.name:
|
||||
case NotAllowedError.name:
|
||||
return 403;
|
||||
case errors.NotFoundError.name:
|
||||
case NotFoundError.name:
|
||||
return 404;
|
||||
case errors.ConflictError.name:
|
||||
case ConflictError.name:
|
||||
return 409;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -26,7 +26,7 @@ import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import * as os from 'os';
|
||||
import path from 'path';
|
||||
import { NotModifiedError } from '../errors';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { getVoidLogger } from '../logging';
|
||||
import { AzureUrlReader } from './AzureUrlReader';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
|
||||
@@ -26,7 +26,7 @@ import fetch from 'cross-fetch';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { Readable } from 'stream';
|
||||
import { NotFoundError, NotModifiedError } from '../errors';
|
||||
import { NotFoundError, NotModifiedError } from '@backstage/errors';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
import { stripFirstDirectoryFromPath } from './tree/util';
|
||||
import {
|
||||
|
||||
@@ -26,7 +26,7 @@ import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { NotModifiedError } from '../errors';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import fetch from 'cross-fetch';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { Readable } from 'stream';
|
||||
import { NotFoundError, NotModifiedError } from '../errors';
|
||||
import { NotFoundError, NotModifiedError } from '@backstage/errors';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
import { stripFirstDirectoryFromPath } from './tree/util';
|
||||
import {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import fetch from 'cross-fetch';
|
||||
import { NotFoundError } from '../errors';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import {
|
||||
ReaderFactory,
|
||||
ReadTreeResponse,
|
||||
|
||||
@@ -27,7 +27,7 @@ import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { NotFoundError, NotModifiedError } from '../errors';
|
||||
import { NotFoundError, NotModifiedError } from '@backstage/errors';
|
||||
import {
|
||||
GhBlobResponse,
|
||||
GhBranchResponse,
|
||||
|
||||
@@ -25,7 +25,7 @@ import fetch from 'cross-fetch';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { Readable } from 'stream';
|
||||
import { NotFoundError, NotModifiedError } from '../errors';
|
||||
import { NotFoundError, NotModifiedError } from '@backstage/errors';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
import {
|
||||
ReaderFactory,
|
||||
|
||||
@@ -25,7 +25,7 @@ import path from 'path';
|
||||
import { getVoidLogger } from '../logging';
|
||||
import { GitlabUrlReader } from './GitlabUrlReader';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
import { NotModifiedError, NotFoundError } from '../errors';
|
||||
import { NotModifiedError, NotFoundError } from '@backstage/errors';
|
||||
import {
|
||||
GitLabIntegration,
|
||||
readGitLabIntegrationConfig,
|
||||
|
||||
@@ -24,7 +24,7 @@ import fetch from 'cross-fetch';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { Readable } from 'stream';
|
||||
import { NotFoundError, NotModifiedError } from '../errors';
|
||||
import { NotFoundError, NotModifiedError } from '@backstage/errors';
|
||||
import { ReadTreeResponseFactory } from './tree';
|
||||
import { stripFirstDirectoryFromPath } from './tree/util';
|
||||
import {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { NotAllowedError } from '../errors';
|
||||
import { NotAllowedError } from '@backstage/errors';
|
||||
import {
|
||||
ReadTreeOptions,
|
||||
ReadTreeResponse,
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.7.4",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"cross-fetch": "^3.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
LOCATION_ANNOTATION,
|
||||
stringifyLocationReference,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ServerResponseError } from '@backstage/errors';
|
||||
import fetch from 'cross-fetch';
|
||||
import {
|
||||
AddLocationRequest,
|
||||
@@ -153,10 +154,7 @@ export class CatalogClient implements CatalogApi {
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
throw new Error(
|
||||
`Request failed with ${response.status} ${response.statusText}, ${payload}`,
|
||||
);
|
||||
throw await ServerResponseError.forResponse(response);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -177,9 +175,7 @@ export class CatalogClient implements CatalogApi {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
throw new Error(message);
|
||||
throw await ServerResponseError.forResponse(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
@@ -200,10 +196,7 @@ export class CatalogClient implements CatalogApi {
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payload = await response.text();
|
||||
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
|
||||
throw new Error(message);
|
||||
throw await ServerResponseError.forResponse(response);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/core-api": "^0.2.13",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/theme": "^0.2.4",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -14,21 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useRef, useState, MouseEventHandler } from 'react';
|
||||
import { IconButton, makeStyles, Tooltip } from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import CopyIcon from '@material-ui/icons/FileCopy';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { errorApiRef, useApi } from '@backstage/core-api';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
button: {
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.highlight,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
}));
|
||||
import { IconButton, Tooltip } from '@material-ui/core';
|
||||
import CopyIcon from '@material-ui/icons/FileCopy';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { MouseEventHandler, useRef, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Copy text button with visual feedback in the form of
|
||||
@@ -61,7 +51,6 @@ export const CopyTextButton = (props: Props) => {
|
||||
...defaultProps,
|
||||
...props,
|
||||
};
|
||||
const classes = useStyles(props);
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -84,7 +73,7 @@ export const CopyTextButton = (props: Props) => {
|
||||
<>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
style={{ position: 'absolute', top: -9999, left: 9999 }}
|
||||
style={{ position: 'absolute', top: -9999, left: -9999 }}
|
||||
defaultValue={text}
|
||||
/>
|
||||
<Tooltip
|
||||
@@ -95,7 +84,7 @@ export const CopyTextButton = (props: Props) => {
|
||||
onClose={() => setOpen(false)}
|
||||
open={open}
|
||||
>
|
||||
<IconButton onClick={handleCopyClick} className={classes.button}>
|
||||
<IconButton onClick={handleCopyClick}>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ServerResponseError } from '@backstage/errors';
|
||||
import {
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { CodeSnippet } from '../CodeSnippet';
|
||||
import { CopyTextButton } from '../CopyTextButton';
|
||||
import { WarningPanel } from '../WarningPanel';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
text: {
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre',
|
||||
overflowX: 'auto',
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
divider: {
|
||||
margin: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
error: Error;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders details about a failed server request.
|
||||
*
|
||||
* Has special treatment for ServerResponseError errors, to display rich
|
||||
* server-provided information about what happened.
|
||||
*/
|
||||
export const ErrorResponse = ({ error }: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
if (error.name !== 'ServerResponseError') {
|
||||
return (
|
||||
<List>
|
||||
<ListItemText primary="Message" secondary={error.message} />
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
const { body } = error as ServerResponseError;
|
||||
const errorString = `${body.error.statusCode}: ${body.error.name}`;
|
||||
const request = body.request && `${body.request.method} ${body.request.url}`;
|
||||
const message = body.error.message.replace(/\\n/g, '\n');
|
||||
const stack = body.error.stack?.replace(/\\n/g, '\n');
|
||||
const json = JSON.stringify(error, undefined, 2);
|
||||
|
||||
return (
|
||||
<>
|
||||
<List dense>
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText
|
||||
classes={{ secondary: classes.text }}
|
||||
primary="Error"
|
||||
secondary={errorString}
|
||||
/>
|
||||
<CopyTextButton text={errorString} />
|
||||
</ListItem>
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText
|
||||
classes={{ secondary: classes.text }}
|
||||
primary="Message"
|
||||
secondary={message}
|
||||
/>
|
||||
<CopyTextButton text={message} />
|
||||
</ListItem>
|
||||
{request ? (
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText
|
||||
classes={{ secondary: classes.text }}
|
||||
primary="Request"
|
||||
secondary={request}
|
||||
/>
|
||||
<CopyTextButton text={request} />
|
||||
</ListItem>
|
||||
) : null}
|
||||
{stack ? (
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText
|
||||
classes={{ secondary: classes.text }}
|
||||
primary="Stack Trace"
|
||||
secondary={stack}
|
||||
/>
|
||||
<CopyTextButton text={stack} />
|
||||
</ListItem>
|
||||
) : null}
|
||||
<Divider component="li" className={classes.divider} />
|
||||
<ListItem alignItems="flex-start">
|
||||
<ListItemText
|
||||
classes={{ secondary: classes.text }}
|
||||
primary="Full Error as JSON"
|
||||
secondary={<CodeSnippet language="json" text={json} />}
|
||||
/>
|
||||
<CopyTextButton text={json} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a warning panel as the effect of a failed server request.
|
||||
*
|
||||
* Has special treatment for ServerResponseError errors, to display rich
|
||||
* server-provided information about what happened.
|
||||
*/
|
||||
export const ErrorResponsePanel = ({ error }: Props) => {
|
||||
return (
|
||||
<WarningPanel title={error.message}>
|
||||
<ErrorResponse error={error} />
|
||||
</WarningPanel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { ErrorResponse, ErrorResponsePanel } from './ErrorResponse';
|
||||
@@ -22,6 +22,7 @@ export * from './CopyTextButton';
|
||||
export * from './DependencyGraph';
|
||||
export * from './DismissableBanner';
|
||||
export * from './EmptyState';
|
||||
export * from './ErrorResponse';
|
||||
export * from './FeatureDiscovery';
|
||||
export * from './HeaderIconLinkRow';
|
||||
export * from './HorizontalScrollGrid';
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"@backstage/cli": "*",
|
||||
"@backstage/config": "*",
|
||||
"@backstage/core": "*",
|
||||
"@backstage/errors": "*",
|
||||
"@backstage/plugin-api-docs": "*",
|
||||
"@backstage/plugin-app-backend": "*",
|
||||
"@backstage/plugin-auth-backend": "*",
|
||||
|
||||
@@ -36,6 +36,7 @@ import { version as catalogModel } from '../../../catalog-model/package.json';
|
||||
import { version as cli } from '../../../cli/package.json';
|
||||
import { version as config } from '../../../config/package.json';
|
||||
import { version as core } from '../../../core/package.json';
|
||||
import { version as errors } from '../../../errors/package.json';
|
||||
import { version as testUtils } from '../../../test-utils/package.json';
|
||||
import { version as theme } from '../../../theme/package.json';
|
||||
|
||||
@@ -67,6 +68,7 @@ export const packageVersions = {
|
||||
'@backstage/cli': cli,
|
||||
'@backstage/config': config,
|
||||
'@backstage/core': core,
|
||||
'@backstage/errors': errors,
|
||||
'@backstage/plugin-api-docs': pluginApiDocs,
|
||||
'@backstage/plugin-app-backend': pluginAppBackend,
|
||||
'@backstage/plugin-auth-backend': pluginAuthBackend,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
# Common error handling functionality
|
||||
|
||||
Contains some common functionality of error handling.
|
||||
|
||||
This package will be imported both by the frontend and backend.
|
||||
|
||||
## Links
|
||||
|
||||
- [The Backstage homepage](https://backstage.io)
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@backstage/errors",
|
||||
"version": "0.1.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "packages/errors"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli build",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^0.1.2",
|
||||
"cross-fetch": "^3.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.6.1",
|
||||
"@types/jest": "^26.0.7"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export class CustomErrorBase extends Error {
|
||||
readonly cause?: Error;
|
||||
|
||||
constructor(message?: string, cause?: Error) {
|
||||
let fullMessage = message;
|
||||
if (cause) {
|
||||
if (fullMessage) {
|
||||
fullMessage += `; caused by ${cause}`;
|
||||
} else {
|
||||
fullMessage = `caused by ${cause}`;
|
||||
}
|
||||
}
|
||||
|
||||
super(fullMessage);
|
||||
|
||||
Error.captureStackTrace?.(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as errors from './errors';
|
||||
import * as errors from './common';
|
||||
|
||||
describe('errors', () => {
|
||||
describe('common', () => {
|
||||
it('extends Error properly', () => {
|
||||
for (const [name, E] of Object.entries(errors)) {
|
||||
const error = new E('abcdef');
|
||||
@@ -30,7 +30,7 @@ describe('errors', () => {
|
||||
|
||||
it('supports causes', () => {
|
||||
const cause = new Error('hello');
|
||||
for (const [, E] of Object.entries(errors)) {
|
||||
for (const [name, E] of Object.entries(errors)) {
|
||||
const error = new E('abcdef', cause);
|
||||
expect(error.cause).toBe(cause);
|
||||
expect(error.toString()).toContain(
|
||||
@@ -14,39 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CustomErrorBase } from './CustomErrorBase';
|
||||
|
||||
/*
|
||||
* A set of common business logic errors.
|
||||
*
|
||||
* The error handler middleware understands these and will translate them to
|
||||
* well formed HTTP responses.
|
||||
* A backend error handler middleware would understand these and translate them
|
||||
* to well formed HTTP responses.
|
||||
*
|
||||
* While these are intentionally analogous to HTTP errors, they are not
|
||||
* intended to be thrown by the request handling layer. In those places, please
|
||||
* use e.g. the http-errors library.
|
||||
*/
|
||||
|
||||
class CustomErrorBase extends Error {
|
||||
readonly cause?: Error;
|
||||
|
||||
constructor(message?: string, cause?: Error) {
|
||||
let fullMessage = message;
|
||||
if (cause) {
|
||||
if (fullMessage) {
|
||||
fullMessage += `; caused by ${cause}`;
|
||||
} else {
|
||||
fullMessage = `caused by ${cause}`;
|
||||
}
|
||||
}
|
||||
|
||||
super(fullMessage);
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The given inputs are malformed and cannot be processed.
|
||||
*/
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
InputError,
|
||||
AuthenticationError,
|
||||
NotAllowedError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
NotModifiedError,
|
||||
} from './common';
|
||||
export { ServerResponseError } from './server';
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
parseServerResponseErrorBody,
|
||||
ServerResponseErrorBody,
|
||||
} from '../payload';
|
||||
|
||||
/**
|
||||
* An error thrown as the result of a failed server request.
|
||||
*
|
||||
* The server is expected to respond on the ServerResponseErrorBody format.
|
||||
*/
|
||||
export class ServerResponseError extends Error {
|
||||
static async forResponse(response: Response): Promise<ServerResponseError> {
|
||||
const body = await parseServerResponseErrorBody(response);
|
||||
const status = body.error.statusCode || response.status;
|
||||
const statusText = body.error.name || response.statusText;
|
||||
const message = `Request failed with ${status} ${statusText}`;
|
||||
return new ServerResponseError(message, body);
|
||||
}
|
||||
|
||||
readonly body: ServerResponseErrorBody;
|
||||
|
||||
constructor(message: string, body: ServerResponseErrorBody) {
|
||||
super(message);
|
||||
this.name = 'ServerResponseError';
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './error';
|
||||
export * from './payload';
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
parseServerResponseErrorBody,
|
||||
ServerResponseErrorBody,
|
||||
} from './ServerResponseErrorBody';
|
||||
|
||||
describe('parseServerResponseErrorBody', () => {
|
||||
it('handles the happy path', async () => {
|
||||
const body: ServerResponseErrorBody = {
|
||||
error: {
|
||||
statusCode: 444,
|
||||
name: 'Fours',
|
||||
message: 'Expected fives',
|
||||
stack: 'lines',
|
||||
},
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
},
|
||||
};
|
||||
|
||||
const response: Partial<Response> = {
|
||||
status: 444,
|
||||
statusText: 'Fours',
|
||||
text: async () => JSON.stringify(body),
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
};
|
||||
|
||||
await expect(
|
||||
parseServerResponseErrorBody(response as Response),
|
||||
).resolves.toEqual(body);
|
||||
});
|
||||
|
||||
it('uses request header and text body when wrong content type, even if parsable', async () => {
|
||||
const body: ServerResponseErrorBody = {
|
||||
error: {
|
||||
statusCode: 333,
|
||||
name: 'Threes',
|
||||
message: 'Expected twos',
|
||||
},
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
},
|
||||
};
|
||||
|
||||
const response: Partial<Response> = {
|
||||
status: 444,
|
||||
statusText: 'Fours',
|
||||
text: async () => JSON.stringify(body),
|
||||
headers: new Headers({ 'Content-Type': 'not-application/not-json' }),
|
||||
};
|
||||
|
||||
await expect(
|
||||
parseServerResponseErrorBody(response as Response),
|
||||
).resolves.toEqual({
|
||||
error: {
|
||||
statusCode: 444,
|
||||
name: 'Unknown',
|
||||
message: `Request failed with status 444 Fours, ${JSON.stringify(
|
||||
body,
|
||||
)}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('uses request header and text body when not parsable', async () => {
|
||||
const body: ServerResponseErrorBody = {
|
||||
error: {
|
||||
statusCode: 333,
|
||||
name: 'Threes',
|
||||
message: 'Expected twos',
|
||||
},
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
},
|
||||
};
|
||||
|
||||
const response: Partial<Response> = {
|
||||
status: 444,
|
||||
statusText: 'Fours',
|
||||
text: async () => JSON.stringify(body).substring(1),
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
};
|
||||
|
||||
await expect(
|
||||
parseServerResponseErrorBody(response as Response),
|
||||
).resolves.toEqual({
|
||||
error: {
|
||||
statusCode: 444,
|
||||
name: 'Unknown',
|
||||
message: `Request failed with status 444 Fours, ${JSON.stringify(
|
||||
body,
|
||||
).substring(1)}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('uses request header when failing to get body', async () => {
|
||||
const response: Partial<Response> = {
|
||||
status: 444,
|
||||
statusText: 'Fours',
|
||||
text: async () => {
|
||||
throw new Error('bail');
|
||||
},
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
};
|
||||
|
||||
await expect(
|
||||
parseServerResponseErrorBody(response as Response),
|
||||
).resolves.toEqual({
|
||||
error: {
|
||||
statusCode: 444,
|
||||
name: 'Unknown',
|
||||
message: `Request failed with status 444 Fours`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A standard shape of JSON data returned as the body of backend errors.
|
||||
*/
|
||||
export type ServerResponseErrorBody = {
|
||||
/** Details of the error that was caught */
|
||||
error: {
|
||||
/** The numeric HTTP status code that was returned */
|
||||
statusCode: number;
|
||||
/** The name of the exception that was thrown */
|
||||
name: string;
|
||||
/** The message of the exception that was thrown */
|
||||
message: string;
|
||||
/** A stringified stack trace, may not be present */
|
||||
stack?: string;
|
||||
};
|
||||
|
||||
/** The incoming request */
|
||||
request?: {
|
||||
/** The HTTP method of the request */
|
||||
method: string;
|
||||
/** The URL of the request (excluding protocol and host/port) */
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to extract the ServerResponseErrorBody out of a server response.
|
||||
* This consumes the body of the response.
|
||||
*
|
||||
* The code is forgiving, and constructs a useful synthetic body as best it can
|
||||
* if the response wasn't on the expected form.
|
||||
*
|
||||
* @param response The response of a failed request
|
||||
*/
|
||||
export async function parseServerResponseErrorBody(
|
||||
response: Response,
|
||||
): Promise<ServerResponseErrorBody> {
|
||||
try {
|
||||
const text = await response.text();
|
||||
if (text) {
|
||||
if (
|
||||
response.headers.get('content-type')?.startsWith('application/json')
|
||||
) {
|
||||
try {
|
||||
const body = JSON.parse(text);
|
||||
if (body.error && body.request) {
|
||||
return body;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: {
|
||||
statusCode: response.status,
|
||||
name: 'Unknown',
|
||||
message: `Request failed with status ${response.status} ${response.statusText}, ${text}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return {
|
||||
error: {
|
||||
statusCode: response.status,
|
||||
name: 'Unknown',
|
||||
message: `Request failed with status ${response.status} ${response.statusText}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { parseServerResponseErrorBody } from './ServerResponseErrorBody';
|
||||
export type { ServerResponseErrorBody } from './ServerResponseErrorBody';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {};
|
||||
@@ -41,6 +41,7 @@
|
||||
"@backstage/backend-common": "^0.5.6",
|
||||
"@backstage/catalog-model": "^0.7.4",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/integration": "^0.5.1",
|
||||
"@google-cloud/storage": "^5.6.0",
|
||||
"@types/dockerode": "^3.2.1",
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Git, InputError, UrlReader } from '@backstage/backend-common';
|
||||
import { Git, UrlReader } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Entity, parseLocationReference } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { NotModifiedError } from '@backstage/backend-common';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
|
||||
@@ -13,11 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
InputError,
|
||||
NotModifiedError,
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { InputError, NotModifiedError } from '@backstage/errors';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { NotModifiedError, UrlReader } from '@backstage/backend-common';
|
||||
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Logger } from 'winston';
|
||||
import { getDocFilesFromRepository } from '../../helpers';
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@backstage/catalog-client": "^0.3.7",
|
||||
"@backstage/catalog-model": "^0.7.4",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/test-utils": "^0.1.8",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/passport": "^1.0.3",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConflictError, NotFoundError } from '@backstage/backend-common';
|
||||
import { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
BackstageIdentity,
|
||||
AuthProviderConfig,
|
||||
} from '../../providers/types';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { verifyNonce } from './helpers';
|
||||
import { postMessageResponse, ensuresXRequestedWith } from '../flow';
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import express from 'express';
|
||||
import { Config } from '@backstage/config';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { readState } from './helpers';
|
||||
import { AuthProviderRouteHandlers } from '../../providers/types';
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ import {
|
||||
AuthProviderFactory,
|
||||
} from '../providers';
|
||||
import {
|
||||
NotFoundError,
|
||||
PluginDatabaseManager,
|
||||
PluginEndpointDiscovery,
|
||||
} from '@backstage/backend-common';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { Config } from '@backstage/config';
|
||||
import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity';
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@backstage/backend-common": "^0.5.6",
|
||||
"@backstage/catalog-model": "^0.7.4",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/integration": "^0.5.1",
|
||||
"@octokit/graphql": "^4.5.8",
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConflictError, NotFoundError } from '@backstage/backend-common';
|
||||
import { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
import {
|
||||
Entity,
|
||||
entityHasChanges,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConflictError } from '@backstage/backend-common';
|
||||
import { ConflictError } from '@backstage/errors';
|
||||
import { Entity, Location, parseEntityRef } from '@backstage/catalog-model';
|
||||
import { EntityFilters } from '../service/EntityFilters';
|
||||
import { DatabaseManager } from './DatabaseManager';
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ConflictError,
|
||||
InputError,
|
||||
NotFoundError,
|
||||
} from '@backstage/backend-common';
|
||||
import { ConflictError, InputError, NotFoundError } from '@backstage/errors';
|
||||
import {
|
||||
Entity,
|
||||
EntityName,
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { NotAllowedError, UrlReader } from '@backstage/backend-common';
|
||||
import { NotAllowedError } from '@backstage/errors';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import {
|
||||
Entity,
|
||||
EntityPolicy,
|
||||
EntityRelationSpec,
|
||||
ENTITY_DEFAULT_NAMESPACE,
|
||||
LocationSpec,
|
||||
stringifyLocationReference,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
@@ -103,7 +105,11 @@ export class LocationReaders implements LocationReader {
|
||||
output.errors.push({
|
||||
location: item.location,
|
||||
error: new NotAllowedError(
|
||||
`Entity of kind ${item.entity.kind} is not allowed from location ${item.location.type} ${item.location.target}`,
|
||||
`Entity of kind ${
|
||||
item.entity.kind
|
||||
} is not allowed from location ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}`,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -166,14 +172,20 @@ export class LocationReaders implements LocationReader {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `Processor ${processor.constructor.name} threw an error while reading location ${item.location.type} ${item.location.target}, ${e}`;
|
||||
const message = `Processor ${
|
||||
processor.constructor.name
|
||||
} threw an error while reading location ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}, ${e}`;
|
||||
emit(result.generalError(item.location, message));
|
||||
logger.warn(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message = `No processor was able to read location ${item.location.type} ${item.location.target}`;
|
||||
const message = `No processor was able to read location ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}`;
|
||||
emit(result.inputError(item.location, message));
|
||||
logger.warn(message);
|
||||
}
|
||||
@@ -205,7 +217,11 @@ export class LocationReaders implements LocationReader {
|
||||
originLocation,
|
||||
);
|
||||
} catch (e) {
|
||||
const message = `Processor ${processor.constructor.name} threw an error while preprocessing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
const message = `Processor ${
|
||||
processor.constructor.name
|
||||
} threw an error while preprocessing entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}, ${e}`;
|
||||
emit(result.generalError(item.location, e.message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
@@ -216,14 +232,18 @@ export class LocationReaders implements LocationReader {
|
||||
try {
|
||||
const next = await this.options.policy.enforce(current);
|
||||
if (!next) {
|
||||
const message = `Policy unexpectedly returned no data while analyzing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}`;
|
||||
const message = `Policy unexpectedly returned no data while analyzing entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}`;
|
||||
emit(result.generalError(item.location, message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
}
|
||||
current = next;
|
||||
} catch (e) {
|
||||
const message = `Policy check failed while analyzing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
const message = `Policy check failed while analyzing entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}, ${e}`;
|
||||
emit(result.inputError(item.location, e.message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
@@ -238,7 +258,11 @@ export class LocationReaders implements LocationReader {
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
const message = `Processor ${processor.constructor.name} threw an error while validating the entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
const message = `Processor ${
|
||||
processor.constructor.name
|
||||
} threw an error while validating the entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}, ${e}`;
|
||||
emit(result.inputError(item.location, message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
@@ -246,7 +270,9 @@ export class LocationReaders implements LocationReader {
|
||||
}
|
||||
}
|
||||
if (!handled) {
|
||||
const message = `No processor recognized the entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}`;
|
||||
const message = `No processor recognized the entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}`;
|
||||
emit(result.inputError(item.location, message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
@@ -261,7 +287,11 @@ export class LocationReaders implements LocationReader {
|
||||
emit,
|
||||
);
|
||||
} catch (e) {
|
||||
const message = `Processor ${processor.constructor.name} threw an error while postprocessing entity ${kind}:${namespace}/${name} at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
const message = `Processor ${
|
||||
processor.constructor.name
|
||||
} threw an error while postprocessing entity ${kind}:${namespace}/${name} at ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}, ${e}`;
|
||||
emit(result.generalError(item.location, message));
|
||||
logger.warn(message);
|
||||
return undefined;
|
||||
@@ -279,7 +309,9 @@ export class LocationReaders implements LocationReader {
|
||||
const { processors, logger } = this.options;
|
||||
|
||||
logger.debug(
|
||||
`Encountered error at location ${item.location.type} ${item.location.target}, ${item.error}`,
|
||||
`Encountered error at location ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}, ${item.error}`,
|
||||
);
|
||||
|
||||
const validatedEmit: CatalogProcessorEmit = emitResult => {
|
||||
@@ -295,7 +327,11 @@ export class LocationReaders implements LocationReader {
|
||||
try {
|
||||
await processor.handleError(item.error, item.location, validatedEmit);
|
||||
} catch (e) {
|
||||
const message = `Processor ${processor.constructor.name} threw an error while handling another error at ${item.location.type} ${item.location.target}, ${e}`;
|
||||
const message = `Processor ${
|
||||
processor.constructor.name
|
||||
} threw an error while handling another error at ${stringifyLocationReference(
|
||||
item.location,
|
||||
)}, ${e}`;
|
||||
emit(result.generalError(item.location, message));
|
||||
logger.warn(message);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { NotFoundError, UrlReader } from '@backstage/backend-common';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import {
|
||||
Entity,
|
||||
LocationSpec,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError, NotFoundError } from '@backstage/backend-common';
|
||||
import { InputError, NotFoundError } from '@backstage/errors';
|
||||
import {
|
||||
Entity,
|
||||
EntityRelationSpec,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { EntitiesSearchFilter, EntityFilter } from '../database';
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import lodash from 'lodash';
|
||||
import { RecursivePartial } from '../util';
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger, NotFoundError } from '@backstage/backend-common';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import type { Entity, LocationSpec } from '@backstage/catalog-model';
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
@@ -14,20 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { errorHandler, NotFoundError } from '@backstage/backend-common';
|
||||
import {
|
||||
locationSpecSchema,
|
||||
analyzeLocationSchema,
|
||||
} from '@backstage/catalog-model';
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import type { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
analyzeLocationSchema,
|
||||
locationSpecSchema,
|
||||
} from '@backstage/catalog-model';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import yn from 'yn';
|
||||
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
|
||||
import { LocationAnalyzer, HigherOrderOperation } from '../ingestion/types';
|
||||
import { translateQueryToFieldMapper } from './filterQuery';
|
||||
import { HigherOrderOperation, LocationAnalyzer } from '../ingestion/types';
|
||||
import { EntityFilters } from './EntityFilters';
|
||||
import { translateQueryToFieldMapper } from './filterQuery';
|
||||
import { requireRequestBody, validateRequestBody } from './util';
|
||||
|
||||
export interface RouterOptions {
|
||||
@@ -105,7 +106,7 @@ export async function createRouter(
|
||||
);
|
||||
if (!entities.length) {
|
||||
throw new NotFoundError(
|
||||
`No entity with kind ${kind} namespace ${namespace} name ${name}`,
|
||||
`No entity named '${name}' found, with kind '${kind}' in namespace '${namespace}'`,
|
||||
);
|
||||
}
|
||||
res.status(200).json(entities[0]);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Request } from 'express';
|
||||
import lodash from 'lodash';
|
||||
import yup from 'yup';
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
Content,
|
||||
ErrorResponsePanel,
|
||||
Header,
|
||||
HeaderLabel,
|
||||
Link,
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
useEntityCompoundName,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Box } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React, { PropsWithChildren, useContext, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu';
|
||||
@@ -143,7 +143,7 @@ export const EntityPageLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
|
||||
{error && (
|
||||
<Content>
|
||||
<Alert severity="error">{error.toString()}</Alert>
|
||||
<ErrorResponsePanel error={error} />
|
||||
</Content>
|
||||
)}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@backstage/catalog-client": "^0.3.7",
|
||||
"@backstage/catalog-model": "^0.7.4",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/integration": "^0.5.1",
|
||||
"@gitbeaker/core": "^28.0.2",
|
||||
"@gitbeaker/node": "^28.0.2",
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
TemplateEntityV1beta2,
|
||||
} from '@backstage/catalog-model';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { ConflictError, NotFoundError } from '@backstage/backend-common';
|
||||
import { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
|
||||
/**
|
||||
* A catalog client tailored for reading out entity data from the catalog.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { InputBase, TemplateAction } from './types';
|
||||
import { ConflictError, NotFoundError } from '@backstage/backend-common';
|
||||
import { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
|
||||
export class TemplateActionRegistry {
|
||||
private readonly actions = new Map<string, TemplateAction<any>>();
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { getEntityName } from '@backstage/catalog-model';
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import Docker from 'dockerode';
|
||||
import { InputError, UrlReader } from '@backstage/backend-common';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import { TemplaterBuilder, TemplaterValues } from '../../../stages/templater';
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath, isAbsolute } from 'path';
|
||||
import { InputError, UrlReader } from '@backstage/backend-common';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { InputError, UrlReader } from '@backstage/backend-common';
|
||||
import { UrlReader } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { fetchContents } from './helpers';
|
||||
import { createTemplateAction } from '../../createTemplateAction';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { initRepoAndPush } from '../../../stages/publish/helpers';
|
||||
import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
BitbucketIntegrationConfig,
|
||||
ScmIntegrationRegistry,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
GithubCredentialsProvider,
|
||||
ScmIntegrationRegistry,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { Gitlab } from '@gitbeaker/node';
|
||||
import { initRepoAndPush } from '../../../stages/publish/helpers';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
|
||||
export const parseRepoUrl = (repoUrl: string) => {
|
||||
let parsed;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import {
|
||||
LOCATION_ANNOTATION,
|
||||
parseLocationReference,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { PreparerBase, PreparerOptions } from './types';
|
||||
|
||||
export class FilePreparer implements PreparerBase {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { spawn } from 'child_process';
|
||||
import { PassThrough, Writable } from 'stream';
|
||||
|
||||
@@ -15,11 +15,8 @@
|
||||
*/
|
||||
|
||||
import { JsonObject } from '@backstage/config';
|
||||
import {
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
resolvePackagePath,
|
||||
} from '@backstage/backend-common';
|
||||
import { resolvePackagePath } from '@backstage/backend-common';
|
||||
import { ConflictError, NotFoundError } from '@backstage/errors';
|
||||
import { Knex } from 'knex';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import {
|
||||
|
||||
@@ -24,7 +24,7 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { TemplateActionRegistry } from '../actions/TemplateActionRegistry';
|
||||
import * as handlebars from 'handlebars';
|
||||
import { InputError } from '@backstage/backend-common';
|
||||
import { InputError } from '@backstage/errors';
|
||||
|
||||
type Options = {
|
||||
logger: Logger;
|
||||
|
||||
@@ -42,12 +42,8 @@ import { templateEntityToSpec } from '../scaffolder/tasks/TemplateConverter';
|
||||
import { TemplateActionRegistry } from '../scaffolder/actions/TemplateActionRegistry';
|
||||
import { createLegacyActions } from '../scaffolder/stages/legacy';
|
||||
import { getEntityBaseUrl, getWorkingDirectory } from './helpers';
|
||||
import {
|
||||
InputError,
|
||||
NotFoundError,
|
||||
PluginDatabaseManager,
|
||||
UrlReader,
|
||||
} from '@backstage/backend-common';
|
||||
import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common';
|
||||
import { InputError, NotFoundError } from '@backstage/errors';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import {
|
||||
TemplateEntityV1alpha1,
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@backstage/backend-common": "^0.5.6",
|
||||
"@backstage/catalog-model": "^0.7.4",
|
||||
"@backstage/config": "^0.1.3",
|
||||
"@backstage/errors": "^0.1.1",
|
||||
"@backstage/techdocs-common": "^0.4.4",
|
||||
"@types/dockerode": "^3.2.1",
|
||||
"@types/express": "^4.17.6",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { NotModifiedError } from '@backstage/backend-common';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { Entity, serializeEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
GeneratorBase,
|
||||
|
||||
Reference in New Issue
Block a user