catalog-client: change entities interface, add fields support (#3296)

This commit is contained in:
Fredrik Adelöw
2020-11-18 19:58:36 +01:00
committed by GitHub
parent 16bb5a0c8e
commit 717e43de14
19 changed files with 298 additions and 235 deletions
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { CatalogClient } from './CatalogClient';
import { Entity } from '@backstage/catalog-model';
import { DiscoveryApi } from './types';
import { CatalogListResponse, DiscoveryApi } from './types';
const server = setupServer();
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
@@ -40,7 +40,7 @@ describe('CatalogClient', () => {
});
describe('getEntities', () => {
const defaultResponse: Entity[] = [
const defaultServiceResponse: Entity[] = [
{
apiVersion: '1',
kind: 'Component',
@@ -58,22 +58,26 @@ describe('CatalogClient', () => {
},
},
];
const defaultResponse: CatalogListResponse<Entity> = {
items: defaultServiceResponse,
};
beforeEach(() => {
server.use(
rest.get(`${mockBaseUrl}/entities`, (_, res, ctx) => {
return res(ctx.json(defaultResponse));
return res(ctx.json(defaultServiceResponse));
}),
);
});
it('should entities from correct endpoint', async () => {
const entities = await client.getEntities();
expect(entities).toEqual(defaultResponse);
const response = await client.getEntities();
expect(response).toEqual(defaultResponse);
});
it('builds entity search filters properly', async () => {
expect.assertions(2);
server.use(
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
expect(req.url.search).toBe('?filter=a=1,b=2,b=3,%C3%B6=%3D');
@@ -81,13 +85,32 @@ describe('CatalogClient', () => {
}),
);
const entities = await client.getEntities({
a: '1',
b: ['2', '3'],
ö: '=',
const response = await client.getEntities({
filter: {
a: '1',
b: ['2', '3'],
ö: '=',
},
});
expect(entities).toEqual([]);
expect(response.items).toEqual([]);
});
it('builds entity field selectors properly', async () => {
expect.assertions(2);
server.use(
rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => {
expect(req.url.search).toBe('?fields=a.b,%C3%B6');
return res(ctx.json([]));
}),
);
const response = await client.getEntities({
fields: ['a.b', 'ö'],
});
expect(response.items).toEqual([]);
});
});
});
+55 -41
View File
@@ -25,6 +25,8 @@ import {
AddLocationRequest,
AddLocationResponse,
CatalogApi,
CatalogEntitiesRequest,
CatalogListResponse,
DiscoveryApi,
} from './types';
@@ -35,55 +37,33 @@ export class CatalogClient implements CatalogApi {
this.discoveryApi = options.discoveryApi;
}
private async getRequired(path: string): Promise<any> {
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
const response = await fetch(url);
if (!response.ok) {
const payload = await response.text();
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
throw new Error(message);
}
return await response.json();
}
private async getOptional(path: string): Promise<any | undefined> {
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
const response = await fetch(url);
if (!response.ok) {
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);
}
return await response.json();
}
async getLocationById(id: String): Promise<Location | undefined> {
return await this.getOptional(`/locations/${id}`);
}
async getEntities(
filter?: Record<string, string | string[]>,
): Promise<Entity[]> {
let path = `/entities`;
if (filter) {
const parts: string[] = [];
for (const [key, value] of Object.entries(filter)) {
for (const v of [value].flat()) {
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
}
request?: CatalogEntitiesRequest,
): Promise<CatalogListResponse<Entity>> {
const { filter = {}, fields = [] } = request ?? {};
const params: string[] = [];
const filterParts: string[] = [];
for (const [key, value] of Object.entries(filter)) {
for (const v of [value].flat()) {
filterParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
}
path += `?filter=${parts.join(',')}`;
}
if (filterParts.length) {
params.push(`filter=${filterParts.join(',')}`);
}
return await this.getRequired(path);
if (fields.length) {
params.push(`fields=${fields.map(encodeURIComponent).join(',')}`);
}
const query = params.length ? `?${params.join('&')}` : '';
const entities: Entity[] = await this.getRequired(`/entities${query}`);
return { items: entities };
}
async getEntityByName(compoundName: EntityName): Promise<Entity | undefined> {
@@ -153,4 +133,38 @@ export class CatalogClient implements CatalogApi {
}
return undefined;
}
//
// Private methods
//
private async getRequired(path: string): Promise<any> {
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
const response = await fetch(url);
if (!response.ok) {
const payload = await response.text();
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
throw new Error(message);
}
return await response.json();
}
private async getOptional(path: string): Promise<any | undefined> {
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
const response = await fetch(url);
if (!response.ok) {
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);
}
return await response.json();
}
}
+12 -1
View File
@@ -16,10 +16,21 @@
import { Entity, EntityName, Location } from '@backstage/catalog-model';
export type CatalogEntitiesRequest = {
filter?: Record<string, string | string[]> | undefined;
fields?: string[] | undefined;
};
export type CatalogListResponse<T> = {
items: T[];
};
export interface CatalogApi {
getLocationById(id: String): Promise<Location | undefined>;
getEntityByName(name: EntityName): Promise<Entity | undefined>;
getEntities(filter?: Record<string, string | string[]>): Promise<Entity[]>;
getEntities(
request?: CatalogEntitiesRequest,
): Promise<CatalogListResponse<Entity>>;
addLocation(location: AddLocationRequest): Promise<AddLocationResponse>;
getLocationByEntity(entity: Entity): Promise<Location | undefined>;
removeEntityByUid(uid: string): Promise<void>;