Merge pull request #29035 from dotboris/catalog-client-get-locations

Add `CatalogApi.getLocations`
This commit is contained in:
Fredrik Adelöw
2025-04-21 14:28:13 +02:00
committed by GitHub
14 changed files with 185 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/catalog-client': minor
'@backstage/plugin-catalog-react': minor
'@backstage/plugin-catalog-node': minor
---
Add `getLocations` method to `CatalogApi` and `CatalogClient`. This method calls the [`GET /locations`](https://backstage.io/docs/features/software-catalog/software-catalog-api/#get-locations) endpoint from the catalog backend.
@@ -16,6 +16,7 @@ import { GetEntityAncestorsRequest } from '@backstage/catalog-client';
import { GetEntityAncestorsResponse } from '@backstage/catalog-client';
import { GetEntityFacetsRequest } from '@backstage/catalog-client';
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
import { GetLocationsResponse } from '@backstage/catalog-client';
import { Location as Location_2 } from '@backstage/catalog-client';
import { QueryEntitiesRequest } from '@backstage/catalog-client';
import { QueryEntitiesResponse } from '@backstage/catalog-client';
@@ -53,6 +54,8 @@ export class InMemoryCatalogClient implements CatalogApi {
// (undocumented)
getLocationByRef(_locationRef: string): Promise<Location_2 | undefined>;
// (undocumented)
getLocations(_request?: {}): Promise<GetLocationsResponse>;
// (undocumented)
queryEntities(request?: QueryEntitiesRequest): Promise<QueryEntitiesResponse>;
// (undocumented)
refreshEntity(_entityRef: string): Promise<void>;
+14
View File
@@ -62,6 +62,10 @@ export interface CatalogApi {
locationRef: string,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
getLocations(
request?: {},
options?: CatalogRequestOptions,
): Promise<GetLocationsResponse>;
queryEntities(
request?: QueryEntitiesRequest,
options?: CatalogRequestOptions,
@@ -136,6 +140,10 @@ export class CatalogClient implements CatalogApi {
locationRef: string,
options?: CatalogRequestOptions,
): Promise<Location_2 | undefined>;
getLocations(
request?: {},
options?: CatalogRequestOptions,
): Promise<GetLocationsResponse>;
queryEntities(
request?: QueryEntitiesRequest,
options?: CatalogRequestOptions,
@@ -250,6 +258,12 @@ export interface GetEntityFacetsResponse {
>;
}
// @public
export interface GetLocationsResponse {
// (undocumented)
items: Location_2[];
}
// @public
type Location_2 = {
id: string;
@@ -24,6 +24,7 @@ import {
QueryEntitiesResponse,
} from './types/api';
import { DiscoveryApi } from './types/discovery';
import { GetLocations200ResponseInner } from './schema/openapi';
const server = setupServer();
const token = 'fake-token';
@@ -593,6 +594,88 @@ describe('CatalogClient', () => {
});
});
describe('getLocations', () => {
const defaultResponse = [
{
data: {
id: '42',
type: 'url',
target: 'https://example.com',
},
},
{
data: {
id: '43',
type: 'url',
target: 'https://example.com',
},
},
] satisfies GetLocations200ResponseInner[];
beforeEach(() => {
server.use(
rest.get(`${mockBaseUrl}/locations`, (_, res, ctx) => {
return res(ctx.json(defaultResponse));
}),
);
});
it('should return locations from correct endpoint', async () => {
const response = await client.getLocations({}, { token });
expect(response).toEqual({
items: [
{
id: '42',
type: 'url',
target: 'https://example.com',
},
{
id: '43',
type: 'url',
target: 'https://example.com',
},
],
});
});
it('should return empty list with empty result', async () => {
server.use(
rest.get(`${mockBaseUrl}/locations`, (_, res, ctx) => {
return res(ctx.json([]));
}),
);
const response = await client.getLocations({}, { token });
expect(response).toEqual({ items: [] });
});
it('should forward token', async () => {
expect.assertions(1);
server.use(
rest.get(`${mockBaseUrl}/locations`, (req, res, ctx) => {
expect(req.headers.get('authorization')).toBe(`Bearer ${token}`);
return res(ctx.json(defaultResponse));
}),
);
await client.getLocations({}, { token });
});
it('should not forward token if omitted', async () => {
expect.assertions(1);
server.use(
rest.get(`${mockBaseUrl}/locations`, (req, res, ctx) => {
expect(req.headers.get('authorization')).toBeNull();
return res(ctx.json(defaultResponse));
}),
);
await client.getLocations();
});
});
describe('getLocationById', () => {
const defaultResponse = {
data: {
@@ -36,6 +36,7 @@ import {
GetEntityAncestorsResponse,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
GetLocationsResponse,
Location,
QueryEntitiesRequest,
QueryEntitiesResponse,
@@ -75,6 +76,21 @@ export class CatalogClient implements CatalogApi {
);
}
/**
* {@inheritdoc CatalogApi.getLocations}
*/
async getLocations(
request?: {},
options?: CatalogRequestOptions,
): Promise<GetLocationsResponse> {
const res = await this.requestRequired(
await this.apiClient.getLocations(request ?? {}, options),
);
return {
items: res.map(item => item.data),
};
}
/**
* {@inheritdoc CatalogApi.getLocationById}
*/
@@ -28,6 +28,7 @@ import {
GetEntityAncestorsResponse,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
GetLocationsResponse,
Location,
QueryEntitiesRequest,
QueryEntitiesResponse,
@@ -228,6 +229,10 @@ export class InMemoryCatalogClient implements CatalogApi {
};
}
async getLocations(_request?: {}): Promise<GetLocationsResponse> {
throw new NotImplementedError('Method not implemented.');
}
async getLocationById(_id: string): Promise<Location | undefined> {
throw new NotImplementedError('Method not implemented.');
}
+20
View File
@@ -349,6 +349,15 @@ export type Location = {
target: string;
};
/**
* The response type for {@link CatalogClient.getLocations}
*
* @public
*/
export interface GetLocationsResponse {
items: Location[];
}
/**
* The request type for {@link CatalogClient.addLocation}.
*
@@ -590,6 +599,17 @@ export interface CatalogApi {
// Locations
/**
* List locations
*
* @param request - Request parameters
* @param options - Additional options
*/
getLocations(
request?: {},
options?: CatalogRequestOptions,
): Promise<GetLocationsResponse>;
/**
* Gets a registered location by its ID.
*
@@ -31,6 +31,7 @@ export type {
GetEntityAncestorsResponse,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
GetLocationsResponse,
Location,
ValidateEntityResponse,
QueryEntitiesCursorRequest,
@@ -19,6 +19,7 @@ import { GetEntityAncestorsRequest } from '@backstage/catalog-client';
import { GetEntityAncestorsResponse } from '@backstage/catalog-client';
import { GetEntityFacetsRequest } from '@backstage/catalog-client';
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
import { GetLocationsResponse } from '@backstage/catalog-client';
import { Location as Location_2 } from '@backstage/catalog-client';
import { QueryEntitiesRequest } from '@backstage/catalog-client';
import { QueryEntitiesResponse } from '@backstage/catalog-client';
@@ -74,6 +75,11 @@ export interface CatalogServiceMock extends CatalogService, CatalogApi {
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
getLocations(
request?: {},
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
): Promise<GetLocationsResponse>;
// (undocumented)
queryEntities(
request?: QueryEntitiesRequest,
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
+6
View File
@@ -19,6 +19,7 @@ import { GetEntityAncestorsRequest } from '@backstage/catalog-client';
import { GetEntityAncestorsResponse } from '@backstage/catalog-client';
import { GetEntityFacetsRequest } from '@backstage/catalog-client';
import { GetEntityFacetsResponse } from '@backstage/catalog-client';
import { GetLocationsResponse } from '@backstage/catalog-client';
import { JsonValue } from '@backstage/types';
import { Location as Location_2 } from '@backstage/catalog-client';
import { LocationEntityV1alpha1 } from '@backstage/catalog-model';
@@ -164,6 +165,11 @@ export interface CatalogService {
options: CatalogServiceRequestOptions,
): Promise<Location_2 | undefined>;
// (undocumented)
getLocations(
request: {} | undefined,
options: CatalogServiceRequestOptions,
): Promise<GetLocationsResponse>;
// (undocumented)
queryEntities(
request: QueryEntitiesRequest | undefined,
options: CatalogServiceRequestOptions,
@@ -35,6 +35,7 @@ import {
GetEntityAncestorsResponse,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
GetLocationsResponse,
Location,
QueryEntitiesRequest,
QueryEntitiesResponse,
@@ -96,6 +97,11 @@ export interface CatalogService {
options: CatalogServiceRequestOptions,
): Promise<GetEntityFacetsResponse>;
getLocations(
request: {} | undefined,
options: CatalogServiceRequestOptions,
): Promise<GetLocationsResponse>;
getLocationById(
id: string,
options: CatalogServiceRequestOptions,
@@ -223,6 +229,16 @@ class DefaultCatalogService implements CatalogService {
);
}
async getLocations(
request: {} | undefined,
options: CatalogServiceRequestOptions,
): Promise<GetLocationsResponse> {
return this.#catalogApi.getLocations(
request,
await this.#getOptions(options),
);
}
async getLocationById(
id: string,
options: CatalogServiceRequestOptions,
@@ -95,6 +95,7 @@ export namespace catalogServiceMock {
removeEntityByUid: jest.fn(),
refreshEntity: jest.fn(),
getEntityFacets: jest.fn(),
getLocations: jest.fn(),
getLocationById: jest.fn(),
getLocationByRef: jest.fn(),
addLocation: jest.fn(),
@@ -27,6 +27,7 @@ import {
GetEntityAncestorsResponse,
GetEntityFacetsRequest,
GetEntityFacetsResponse,
GetLocationsResponse,
Location,
QueryEntitiesRequest,
QueryEntitiesResponse,
@@ -90,6 +91,11 @@ export interface CatalogServiceMock extends CatalogService, CatalogApi {
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
): Promise<GetEntityFacetsResponse>;
getLocations(
request?: {},
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
): Promise<GetLocationsResponse>;
getLocationById(
id: string,
options?: CatalogServiceRequestOptions | CatalogRequestOptions,
@@ -94,6 +94,7 @@ export namespace catalogApiMock {
removeEntityByUid: jest.fn(),
refreshEntity: jest.fn(),
getEntityFacets: jest.fn(),
getLocations: jest.fn(),
getLocationById: jest.fn(),
getLocationByRef: jest.fn(),
addLocation: jest.fn(),