diff --git a/.changeset/yellow-cows-tickle.md b/.changeset/yellow-cows-tickle.md new file mode 100644 index 0000000000..026c7444f0 --- /dev/null +++ b/.changeset/yellow-cows-tickle.md @@ -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. diff --git a/packages/catalog-client/report-testUtils.api.md b/packages/catalog-client/report-testUtils.api.md index 88d14b4f7a..69aa26ae4d 100644 --- a/packages/catalog-client/report-testUtils.api.md +++ b/packages/catalog-client/report-testUtils.api.md @@ -53,6 +53,8 @@ export class InMemoryCatalogClient implements CatalogApi { // (undocumented) getLocationByRef(_locationRef: string): Promise; // (undocumented) + getLocations(): Promise; + // (undocumented) queryEntities(request?: QueryEntitiesRequest): Promise; // (undocumented) refreshEntity(_entityRef: string): Promise; diff --git a/packages/catalog-client/report.api.md b/packages/catalog-client/report.api.md index 203028edce..411db273de 100644 --- a/packages/catalog-client/report.api.md +++ b/packages/catalog-client/report.api.md @@ -62,6 +62,7 @@ export interface CatalogApi { locationRef: string, options?: CatalogRequestOptions, ): Promise; + getLocations(options?: CatalogRequestOptions): Promise; queryEntities( request?: QueryEntitiesRequest, options?: CatalogRequestOptions, @@ -136,6 +137,7 @@ export class CatalogClient implements CatalogApi { locationRef: string, options?: CatalogRequestOptions, ): Promise; + getLocations(options?: CatalogRequestOptions): Promise; queryEntities( request?: QueryEntitiesRequest, options?: CatalogRequestOptions, diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index ea61634a8b..ad64c194ce 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -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,86 @@ 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([ + { + 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([]); + }); + + 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: { diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index e820716945..93c2acfd77 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -75,6 +75,16 @@ export class CatalogClient implements CatalogApi { ); } + /** + * {@inheritdoc CatalogApi.getLocations} + */ + async getLocations(options?: CatalogRequestOptions): Promise { + const res = await this.requestRequired( + await this.apiClient.getLocations({}, options), + ); + return res.map(item => item.data); + } + /** * {@inheritdoc CatalogApi.getLocationById} */ diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index 13ce190e88..8c09c4ae98 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -228,6 +228,10 @@ export class InMemoryCatalogClient implements CatalogApi { }; } + async getLocations(): Promise { + throw new NotImplementedError('Method not implemented.'); + } + async getLocationById(_id: string): Promise { throw new NotImplementedError('Method not implemented.'); } diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 85cea17660..86c957fdf4 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -590,6 +590,13 @@ export interface CatalogApi { // Locations + /** + * List locations + * + * @param options - Additional options + */ + getLocations(options?: CatalogRequestOptions): Promise; + /** * Gets a registered location by its ID. * diff --git a/plugins/catalog-node/report-testUtils.api.md b/plugins/catalog-node/report-testUtils.api.md index 35dee113c0..46a4777f51 100644 --- a/plugins/catalog-node/report-testUtils.api.md +++ b/plugins/catalog-node/report-testUtils.api.md @@ -74,6 +74,10 @@ export interface CatalogServiceMock extends CatalogService, CatalogApi { options?: CatalogServiceRequestOptions | CatalogRequestOptions, ): Promise; // (undocumented) + getLocations( + options?: CatalogServiceRequestOptions | CatalogRequestOptions, + ): Promise; + // (undocumented) queryEntities( request?: QueryEntitiesRequest, options?: CatalogServiceRequestOptions | CatalogRequestOptions, diff --git a/plugins/catalog-node/report.api.md b/plugins/catalog-node/report.api.md index a51212e412..42819dd3d6 100644 --- a/plugins/catalog-node/report.api.md +++ b/plugins/catalog-node/report.api.md @@ -164,6 +164,8 @@ export interface CatalogService { options: CatalogServiceRequestOptions, ): Promise; // (undocumented) + getLocations(options: CatalogServiceRequestOptions): Promise; + // (undocumented) queryEntities( request: QueryEntitiesRequest | undefined, options: CatalogServiceRequestOptions, diff --git a/plugins/catalog-node/src/catalogService.ts b/plugins/catalog-node/src/catalogService.ts index 5e8c920dad..eb2d59296c 100644 --- a/plugins/catalog-node/src/catalogService.ts +++ b/plugins/catalog-node/src/catalogService.ts @@ -96,6 +96,8 @@ export interface CatalogService { options: CatalogServiceRequestOptions, ): Promise; + getLocations(options: CatalogServiceRequestOptions): Promise; + getLocationById( id: string, options: CatalogServiceRequestOptions, @@ -223,6 +225,12 @@ class DefaultCatalogService implements CatalogService { ); } + async getLocations( + options: CatalogServiceRequestOptions, + ): Promise { + return this.#catalogApi.getLocations(await this.#getOptions(options)); + } + async getLocationById( id: string, options: CatalogServiceRequestOptions, diff --git a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts index 2e8a772dc3..ec469fc927 100644 --- a/plugins/catalog-node/src/testUtils/catalogServiceMock.ts +++ b/plugins/catalog-node/src/testUtils/catalogServiceMock.ts @@ -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(), diff --git a/plugins/catalog-node/src/testUtils/types.ts b/plugins/catalog-node/src/testUtils/types.ts index 1fbbec98f1..6847d754a9 100644 --- a/plugins/catalog-node/src/testUtils/types.ts +++ b/plugins/catalog-node/src/testUtils/types.ts @@ -90,6 +90,10 @@ export interface CatalogServiceMock extends CatalogService, CatalogApi { options?: CatalogServiceRequestOptions | CatalogRequestOptions, ): Promise; + getLocations( + options?: CatalogServiceRequestOptions | CatalogRequestOptions, + ): Promise; + getLocationById( id: string, options?: CatalogServiceRequestOptions | CatalogRequestOptions, diff --git a/plugins/catalog-react/src/testUtils/catalogApiMock.ts b/plugins/catalog-react/src/testUtils/catalogApiMock.ts index 8cf3b740b0..08520e2a9b 100644 --- a/plugins/catalog-react/src/testUtils/catalogApiMock.ts +++ b/plugins/catalog-react/src/testUtils/catalogApiMock.ts @@ -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(),