Merge pull request #13583 from taras/tm/add-verify-entity-to-catalog-client
Add validateEntity to catalog client
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-client': minor
|
||||
---
|
||||
|
||||
Adding `validateEntity` method that calls `/validate-entity` endpoint.
|
||||
@@ -5,6 +5,7 @@
|
||||
```ts
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { SerializedError } from '@backstage/errors';
|
||||
|
||||
// @public
|
||||
export type AddLocationRequest = {
|
||||
@@ -65,6 +66,11 @@ export interface CatalogApi {
|
||||
id: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void>;
|
||||
validateEntity(
|
||||
entity: Entity,
|
||||
location: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<ValidateEntityResponse>;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -122,6 +128,11 @@ export class CatalogClient implements CatalogApi {
|
||||
id: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void>;
|
||||
validateEntity(
|
||||
entity: Entity,
|
||||
location: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<ValidateEntityResponse>;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -196,4 +207,14 @@ type Location_2 = {
|
||||
target: string;
|
||||
};
|
||||
export { Location_2 as Location };
|
||||
|
||||
// @public
|
||||
export type ValidateEntityResponse =
|
||||
| {
|
||||
valid: true;
|
||||
}
|
||||
| {
|
||||
valid: false;
|
||||
errors: SerializedError[];
|
||||
};
|
||||
```
|
||||
|
||||
@@ -295,4 +295,87 @@ describe('CatalogClient', () => {
|
||||
await client.getLocationById('42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEntity', () => {
|
||||
it('returns valid false when validation fails', async () => {
|
||||
server.use(
|
||||
rest.post(`${mockBaseUrl}/validate-entity`, (_req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(400),
|
||||
ctx.json({
|
||||
errors: [
|
||||
{
|
||||
message: 'Missing name',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
await client.validateEntity(
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: '',
|
||||
},
|
||||
},
|
||||
'http://example.com',
|
||||
),
|
||||
).toMatchObject({
|
||||
valid: false,
|
||||
errors: [
|
||||
{
|
||||
message: 'Missing name',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns valid true when validation fails', async () => {
|
||||
server.use(
|
||||
rest.post(`${mockBaseUrl}/validate-entity`, (_req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.text(''));
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
await client.validateEntity(
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'good',
|
||||
},
|
||||
},
|
||||
'http://example.com',
|
||||
),
|
||||
).toMatchObject({
|
||||
valid: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws unexpected error', async () => {
|
||||
server.use(
|
||||
rest.post(`${mockBaseUrl}/validate-entity`, (_req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.json({}));
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(() =>
|
||||
client.validateEntity(
|
||||
{
|
||||
apiVersion: '1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'good',
|
||||
},
|
||||
},
|
||||
'http://example.com',
|
||||
),
|
||||
).rejects.toThrow(/Request failed with 500 Error/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
Location,
|
||||
GetEntityFacetsRequest,
|
||||
GetEntityFacetsResponse,
|
||||
ValidateEntityResponse,
|
||||
} from './types/api';
|
||||
import { DiscoveryApi } from './types/discovery';
|
||||
import { FetchApi } from './types/fetch';
|
||||
@@ -353,6 +354,44 @@ export class CatalogClient implements CatalogApi {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc CatalogApi.validateEntity}
|
||||
*/
|
||||
async validateEntity(
|
||||
entity: Entity,
|
||||
location: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<ValidateEntityResponse> {
|
||||
const response = await this.fetchApi.fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/validate-entity`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options?.token && { Authorization: `Bearer ${options?.token}` }),
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ entity, location }),
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
return {
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status !== 400) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
const { errors = [] } = await response.json();
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Private methods
|
||||
//
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { CompoundEntityRef, Entity } from '@backstage/catalog-model';
|
||||
import { SerializedError } from '@backstage/errors';
|
||||
|
||||
/**
|
||||
* This symbol can be used in place of a value when passed to filters in e.g.
|
||||
@@ -269,6 +270,15 @@ export type AddLocationResponse = {
|
||||
exists?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The response type for {@link CatalogClient.validateEntity}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ValidateEntityResponse =
|
||||
| { valid: true }
|
||||
| { valid: false; errors: SerializedError[] };
|
||||
|
||||
/**
|
||||
* A client for interacting with the Backstage software catalog through its API.
|
||||
*
|
||||
@@ -390,4 +400,16 @@ export interface CatalogApi {
|
||||
id: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Validate entity and its location.
|
||||
*
|
||||
* @param entity - Entity to validate
|
||||
* @param location - URL location of the entity
|
||||
*/
|
||||
validateEntity(
|
||||
entity: Entity,
|
||||
location: string,
|
||||
options?: CatalogRequestOptions,
|
||||
): Promise<ValidateEntityResponse>;
|
||||
}
|
||||
|
||||
@@ -27,5 +27,6 @@ export type {
|
||||
Location,
|
||||
GetEntityFacetsRequest,
|
||||
GetEntityFacetsResponse,
|
||||
ValidateEntityResponse,
|
||||
} from './api';
|
||||
export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status';
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('CatalogIdentityClient', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
const tokenManager: jest.Mocked<TokenManager> = {
|
||||
getToken: jest.fn(),
|
||||
|
||||
@@ -68,6 +68,7 @@ describe('createRouter', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
|
||||
config = new ConfigReader({
|
||||
|
||||
@@ -59,6 +59,7 @@ describe('<CatalogGraphCard/>', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
apis = TestApiRegistry.from([catalogApiRef, catalog]);
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ describe('<CatalogGraphPage/>', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
|
||||
wrapper = (
|
||||
|
||||
+1
@@ -119,6 +119,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
|
||||
Wrapper = ({ children }) => (
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('useEntityStore', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
|
||||
useApi.mockReturnValue(catalogApi);
|
||||
|
||||
@@ -100,6 +100,7 @@ describe('CatalogImportClient', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
|
||||
let catalogImportClient: CatalogImportClient;
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ describe('<StepPrepareCreatePullRequest />', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
|
||||
const errorApi: jest.Mocked<typeof errorApiRef.T> = {
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('<DefaultExplorePage />', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('<DomainExplorerContent />', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -33,6 +33,7 @@ describe('<GroupsExplorerContent />', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('<FossaPage />', () => {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
const fossaApi: jest.Mocked<FossaApi> = {
|
||||
getFindingSummary: jest.fn(),
|
||||
|
||||
@@ -53,6 +53,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked<CatalogApi> {
|
||||
refreshEntity: jest.fn(),
|
||||
getEntityAncestors: jest.fn(),
|
||||
getEntityFacets: jest.fn(),
|
||||
validateEntity: jest.fn(),
|
||||
};
|
||||
if (entity) {
|
||||
mock.getEntityByRef.mockReturnValue(entity);
|
||||
|
||||
Reference in New Issue
Block a user