From 1a003ff1a30530d9d6e9e0904faab426d5d04027 Mon Sep 17 00:00:00 2001 From: Boris Bera Date: Mon, 3 Mar 2025 15:04:50 -0500 Subject: [PATCH 1/2] Add `CatalogApi.getLocations` Signed-off-by: Boris Bera --- .changeset/yellow-cows-tickle.md | 7 ++ .../catalog-client/report-testUtils.api.md | 2 + packages/catalog-client/report.api.md | 2 + .../catalog-client/src/CatalogClient.test.ts | 81 +++++++++++++++++++ packages/catalog-client/src/CatalogClient.ts | 10 +++ .../src/testUtils/InMemoryCatalogClient.ts | 4 + packages/catalog-client/src/types/api.ts | 7 ++ plugins/catalog-node/report-testUtils.api.md | 4 + plugins/catalog-node/report.api.md | 2 + plugins/catalog-node/src/catalogService.ts | 8 ++ .../src/testUtils/catalogServiceMock.ts | 1 + plugins/catalog-node/src/testUtils/types.ts | 4 + .../src/testUtils/catalogApiMock.ts | 1 + 13 files changed, 133 insertions(+) create mode 100644 .changeset/yellow-cows-tickle.md 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(), From d9347c63c71c23f29a478a9b21571ef6cbc99d27 Mon Sep 17 00:00:00 2001 From: Boris Bera Date: Mon, 31 Mar 2025 13:07:16 -0400 Subject: [PATCH 2/2] Leave room for pagination later on Signed-off-by: Boris Bera --- .../catalog-client/report-testUtils.api.md | 3 +- packages/catalog-client/report.api.md | 16 +++++++-- .../catalog-client/src/CatalogClient.test.ts | 34 ++++++++++--------- packages/catalog-client/src/CatalogClient.ts | 12 +++++-- .../src/testUtils/InMemoryCatalogClient.ts | 3 +- packages/catalog-client/src/types/api.ts | 15 +++++++- packages/catalog-client/src/types/index.ts | 1 + plugins/catalog-node/report-testUtils.api.md | 4 ++- plugins/catalog-node/report.api.md | 6 +++- plugins/catalog-node/src/catalogService.ts | 14 ++++++-- plugins/catalog-node/src/testUtils/types.ts | 4 ++- 11 files changed, 82 insertions(+), 30 deletions(-) diff --git a/packages/catalog-client/report-testUtils.api.md b/packages/catalog-client/report-testUtils.api.md index 69aa26ae4d..bb6a8dd469 100644 --- a/packages/catalog-client/report-testUtils.api.md +++ b/packages/catalog-client/report-testUtils.api.md @@ -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,7 +54,7 @@ export class InMemoryCatalogClient implements CatalogApi { // (undocumented) getLocationByRef(_locationRef: string): Promise; // (undocumented) - getLocations(): Promise; + getLocations(_request?: {}): Promise; // (undocumented) queryEntities(request?: QueryEntitiesRequest): Promise; // (undocumented) diff --git a/packages/catalog-client/report.api.md b/packages/catalog-client/report.api.md index 411db273de..0a37ed7e42 100644 --- a/packages/catalog-client/report.api.md +++ b/packages/catalog-client/report.api.md @@ -62,7 +62,10 @@ export interface CatalogApi { locationRef: string, options?: CatalogRequestOptions, ): Promise; - getLocations(options?: CatalogRequestOptions): Promise; + getLocations( + request?: {}, + options?: CatalogRequestOptions, + ): Promise; queryEntities( request?: QueryEntitiesRequest, options?: CatalogRequestOptions, @@ -137,7 +140,10 @@ export class CatalogClient implements CatalogApi { locationRef: string, options?: CatalogRequestOptions, ): Promise; - getLocations(options?: CatalogRequestOptions): Promise; + getLocations( + request?: {}, + options?: CatalogRequestOptions, + ): Promise; queryEntities( request?: QueryEntitiesRequest, options?: CatalogRequestOptions, @@ -252,6 +258,12 @@ export interface GetEntityFacetsResponse { >; } +// @public +export interface GetLocationsResponse { + // (undocumented) + items: Location_2[]; +} + // @public type Location_2 = { id: string; diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index ad64c194ce..abafeb18a5 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -621,19 +621,21 @@ describe('CatalogClient', () => { }); 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', - }, - ]); + 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 () => { @@ -643,8 +645,8 @@ describe('CatalogClient', () => { }), ); - const response = await client.getLocations({ token }); - expect(response).toEqual([]); + const response = await client.getLocations({}, { token }); + expect(response).toEqual({ items: [] }); }); it('should forward token', async () => { @@ -657,7 +659,7 @@ describe('CatalogClient', () => { }), ); - await client.getLocations({ token }); + await client.getLocations({}, { token }); }); it('should not forward token if omitted', async () => { diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 93c2acfd77..b3f8df1aa6 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -36,6 +36,7 @@ import { GetEntityAncestorsResponse, GetEntityFacetsRequest, GetEntityFacetsResponse, + GetLocationsResponse, Location, QueryEntitiesRequest, QueryEntitiesResponse, @@ -78,11 +79,16 @@ export class CatalogClient implements CatalogApi { /** * {@inheritdoc CatalogApi.getLocations} */ - async getLocations(options?: CatalogRequestOptions): Promise { + async getLocations( + request?: {}, + options?: CatalogRequestOptions, + ): Promise { const res = await this.requestRequired( - await this.apiClient.getLocations({}, options), + await this.apiClient.getLocations(request ?? {}, options), ); - return res.map(item => item.data); + return { + items: res.map(item => item.data), + }; } /** diff --git a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts index 8c09c4ae98..a7dd333729 100644 --- a/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts +++ b/packages/catalog-client/src/testUtils/InMemoryCatalogClient.ts @@ -28,6 +28,7 @@ import { GetEntityAncestorsResponse, GetEntityFacetsRequest, GetEntityFacetsResponse, + GetLocationsResponse, Location, QueryEntitiesRequest, QueryEntitiesResponse, @@ -228,7 +229,7 @@ export class InMemoryCatalogClient implements CatalogApi { }; } - async getLocations(): Promise { + async getLocations(_request?: {}): 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 86c957fdf4..c999b29390 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -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}. * @@ -593,9 +602,13 @@ export interface CatalogApi { /** * List locations * + * @param request - Request parameters * @param options - Additional options */ - getLocations(options?: CatalogRequestOptions): Promise; + getLocations( + request?: {}, + options?: CatalogRequestOptions, + ): Promise; /** * Gets a registered location by its ID. diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 9bf35886c8..73b16f4554 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -31,6 +31,7 @@ export type { GetEntityAncestorsResponse, GetEntityFacetsRequest, GetEntityFacetsResponse, + GetLocationsResponse, Location, ValidateEntityResponse, QueryEntitiesCursorRequest, diff --git a/plugins/catalog-node/report-testUtils.api.md b/plugins/catalog-node/report-testUtils.api.md index 46a4777f51..3098888fcb 100644 --- a/plugins/catalog-node/report-testUtils.api.md +++ b/plugins/catalog-node/report-testUtils.api.md @@ -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'; @@ -75,8 +76,9 @@ export interface CatalogServiceMock extends CatalogService, CatalogApi { ): Promise; // (undocumented) getLocations( + request?: {}, options?: CatalogServiceRequestOptions | CatalogRequestOptions, - ): Promise; + ): Promise; // (undocumented) queryEntities( request?: QueryEntitiesRequest, diff --git a/plugins/catalog-node/report.api.md b/plugins/catalog-node/report.api.md index 42819dd3d6..f2fe86a38c 100644 --- a/plugins/catalog-node/report.api.md +++ b/plugins/catalog-node/report.api.md @@ -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,7 +165,10 @@ export interface CatalogService { options: CatalogServiceRequestOptions, ): Promise; // (undocumented) - getLocations(options: CatalogServiceRequestOptions): Promise; + getLocations( + request: {} | undefined, + options: CatalogServiceRequestOptions, + ): Promise; // (undocumented) queryEntities( request: QueryEntitiesRequest | undefined, diff --git a/plugins/catalog-node/src/catalogService.ts b/plugins/catalog-node/src/catalogService.ts index eb2d59296c..51bb1d92e5 100644 --- a/plugins/catalog-node/src/catalogService.ts +++ b/plugins/catalog-node/src/catalogService.ts @@ -35,6 +35,7 @@ import { GetEntityAncestorsResponse, GetEntityFacetsRequest, GetEntityFacetsResponse, + GetLocationsResponse, Location, QueryEntitiesRequest, QueryEntitiesResponse, @@ -96,7 +97,10 @@ export interface CatalogService { options: CatalogServiceRequestOptions, ): Promise; - getLocations(options: CatalogServiceRequestOptions): Promise; + getLocations( + request: {} | undefined, + options: CatalogServiceRequestOptions, + ): Promise; getLocationById( id: string, @@ -226,9 +230,13 @@ class DefaultCatalogService implements CatalogService { } async getLocations( + request: {} | undefined, options: CatalogServiceRequestOptions, - ): Promise { - return this.#catalogApi.getLocations(await this.#getOptions(options)); + ): Promise { + return this.#catalogApi.getLocations( + request, + await this.#getOptions(options), + ); } async getLocationById( diff --git a/plugins/catalog-node/src/testUtils/types.ts b/plugins/catalog-node/src/testUtils/types.ts index 6847d754a9..6e6331a630 100644 --- a/plugins/catalog-node/src/testUtils/types.ts +++ b/plugins/catalog-node/src/testUtils/types.ts @@ -27,6 +27,7 @@ import { GetEntityAncestorsResponse, GetEntityFacetsRequest, GetEntityFacetsResponse, + GetLocationsResponse, Location, QueryEntitiesRequest, QueryEntitiesResponse, @@ -91,8 +92,9 @@ export interface CatalogServiceMock extends CatalogService, CatalogApi { ): Promise; getLocations( + request?: {}, options?: CatalogServiceRequestOptions | CatalogRequestOptions, - ): Promise; + ): Promise; getLocationById( id: string,