Add token to CatalogClient and add wrapper class for frontend
This commit is contained in:
@@ -18,29 +18,16 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { CatalogClient } from './CatalogClient';
|
||||
import { CatalogListResponse, DiscoveryApi, IdentityApi } from './types';
|
||||
import { CatalogListResponse, DiscoveryApi } from './types';
|
||||
|
||||
const server = setupServer();
|
||||
const token = 'fake-token';
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discoveryApi: DiscoveryApi = {
|
||||
async getBaseUrl(_pluginId) {
|
||||
return mockBaseUrl;
|
||||
},
|
||||
};
|
||||
const identityApi: IdentityApi = {
|
||||
getUserId() {
|
||||
return 'jane-fonda';
|
||||
},
|
||||
getProfile() {
|
||||
return { email: 'jane-fonda@spotify.com' };
|
||||
},
|
||||
async getIdToken() {
|
||||
return Promise.resolve('fake-id-token');
|
||||
},
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
describe('CatalogClient', () => {
|
||||
let client: CatalogClient;
|
||||
@@ -50,7 +37,7 @@ describe('CatalogClient', () => {
|
||||
afterEach(() => server.resetHandlers());
|
||||
|
||||
beforeEach(() => {
|
||||
client = new CatalogClient({ discoveryApi, identityApi });
|
||||
client = new CatalogClient({ discoveryApi });
|
||||
});
|
||||
|
||||
describe('getEntities', () => {
|
||||
@@ -85,7 +72,7 @@ describe('CatalogClient', () => {
|
||||
});
|
||||
|
||||
it('should entities from correct endpoint', async () => {
|
||||
const response = await client.getEntities();
|
||||
const response = await client.getEntities(token);
|
||||
expect(response).toEqual(defaultResponse);
|
||||
});
|
||||
|
||||
@@ -99,7 +86,7 @@ describe('CatalogClient', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await client.getEntities({
|
||||
const response = await client.getEntities(token, {
|
||||
filter: {
|
||||
a: '1',
|
||||
b: ['2', '3'],
|
||||
@@ -120,7 +107,7 @@ describe('CatalogClient', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await client.getEntities({
|
||||
const response = await client.getEntities(token, {
|
||||
fields: ['a.b', 'ö'],
|
||||
});
|
||||
|
||||
|
||||
@@ -24,30 +24,27 @@ import fetch from 'cross-fetch';
|
||||
import {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogApi,
|
||||
CatalogEntitiesRequest,
|
||||
CatalogListResponse,
|
||||
DiscoveryApi,
|
||||
IdentityApi,
|
||||
} from './types';
|
||||
|
||||
export class CatalogClient implements CatalogApi {
|
||||
export class CatalogClient {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly identityApi: IdentityApi;
|
||||
|
||||
constructor(options: {
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
}) {
|
||||
constructor(options: { discoveryApi: DiscoveryApi }) {
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
async getLocationById(id: String): Promise<Location | undefined> {
|
||||
return await this.getOptional(`/locations/${id}`);
|
||||
async getLocationById(
|
||||
token: string | undefined,
|
||||
id: String,
|
||||
): Promise<Location | undefined> {
|
||||
return await this.getOptional(token, `/locations/${id}`);
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
token: string | undefined,
|
||||
request?: CatalogEntitiesRequest,
|
||||
): Promise<CatalogListResponse<Entity>> {
|
||||
const { filter = {}, fields = [] } = request ?? {};
|
||||
@@ -68,28 +65,35 @@ export class CatalogClient implements CatalogApi {
|
||||
}
|
||||
|
||||
const query = params.length ? `?${params.join('&')}` : '';
|
||||
const entities: Entity[] = await this.getRequired(`/entities${query}`);
|
||||
const entities: Entity[] = await this.getRequired(
|
||||
token,
|
||||
`/entities${query}`,
|
||||
);
|
||||
return { items: entities };
|
||||
}
|
||||
|
||||
async getEntityByName(compoundName: EntityName): Promise<Entity | undefined> {
|
||||
async getEntityByName(
|
||||
token: string | undefined,
|
||||
compoundName: EntityName,
|
||||
): Promise<Entity | undefined> {
|
||||
const { kind, namespace = 'default', name } = compoundName;
|
||||
return this.getOptional(`/entities/by-name/${kind}/${namespace}/${name}`);
|
||||
return this.getOptional(
|
||||
token,
|
||||
`/entities/by-name/${kind}/${namespace}/${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
async addLocation({
|
||||
type = 'url',
|
||||
target,
|
||||
dryRun,
|
||||
}: AddLocationRequest): Promise<AddLocationResponse> {
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
async addLocation(
|
||||
token: string | undefined,
|
||||
{ type = 'url', target, dryRun }: AddLocationRequest,
|
||||
): Promise<AddLocationResponse> {
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/locations${
|
||||
dryRun ? '?dryRun=true' : ''
|
||||
}`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
@@ -118,21 +122,29 @@ export class CatalogClient implements CatalogApi {
|
||||
};
|
||||
}
|
||||
|
||||
async getLocationByEntity(entity: Entity): Promise<Location | undefined> {
|
||||
async getLocationByEntity(
|
||||
token: string | undefined,
|
||||
entity: Entity,
|
||||
): Promise<Location | undefined> {
|
||||
const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION];
|
||||
const all: { data: Location }[] = await this.getRequired('/locations');
|
||||
const all: { data: Location }[] = await this.getRequired(
|
||||
token,
|
||||
'/locations',
|
||||
);
|
||||
return all
|
||||
.map(r => r.data)
|
||||
.find(l => locationCompound === `${l.type}:${l.target}`);
|
||||
}
|
||||
|
||||
async removeEntityByUid(uid: string): Promise<void> {
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
async removeEntityByUid(
|
||||
token: string | undefined,
|
||||
uid: string,
|
||||
): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${await this.discoveryApi.getBaseUrl('catalog')}/entities/by-uid/${uid}`,
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
method: 'DELETE',
|
||||
},
|
||||
@@ -150,13 +162,13 @@ export class CatalogClient implements CatalogApi {
|
||||
// Private methods
|
||||
//
|
||||
|
||||
private async getRequired(path: string): Promise<any> {
|
||||
private async getRequired(
|
||||
token: string | undefined,
|
||||
path: string,
|
||||
): Promise<any> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
},
|
||||
headers: token ? { authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -168,13 +180,13 @@ export class CatalogClient implements CatalogApi {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
private async getOptional(path: string): Promise<any | undefined> {
|
||||
private async getOptional(
|
||||
token: string | undefined,
|
||||
path: string,
|
||||
): Promise<any | undefined> {
|
||||
const url = `${await this.discoveryApi.getBaseUrl('catalog')}${path}`;
|
||||
const idToken = await this.identityApi.getIdToken();
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
authorization: `Bearer ${idToken}`,
|
||||
},
|
||||
headers: token ? { authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -15,4 +15,10 @@
|
||||
*/
|
||||
|
||||
export { CatalogClient } from './CatalogClient';
|
||||
export type { CatalogApi } from './types';
|
||||
export type {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogApi,
|
||||
CatalogEntitiesRequest,
|
||||
CatalogListResponse,
|
||||
} from './types';
|
||||
|
||||
@@ -53,17 +53,3 @@ export type AddLocationResponse = {
|
||||
export type DiscoveryApi = {
|
||||
getBaseUrl(pluginId: string): Promise<string>;
|
||||
};
|
||||
/**
|
||||
* This is a copy of the core IdentityApi, to avoid importing core.
|
||||
*/
|
||||
export type ProfileInfo = {
|
||||
email?: string;
|
||||
displayName?: string;
|
||||
picture?: string;
|
||||
};
|
||||
export type IdentityApi = {
|
||||
getUserId(): string;
|
||||
getProfile(): ProfileInfo;
|
||||
getIdToken(): Promise<string | undefined>;
|
||||
signOut(): Promise<void>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core';
|
||||
import { CatalogClientWrapper } from './CatalogClientWrapper';
|
||||
|
||||
jest.mock('./CatalogClient');
|
||||
const MockedCatalogClient = CatalogClient as jest.Mock<CatalogClient>;
|
||||
|
||||
const mockBaseUrl = 'http://backstage:9191/i-am-a-mock-base';
|
||||
const discoveryApi: DiscoveryApi = {
|
||||
async getBaseUrl(_pluginId) {
|
||||
return mockBaseUrl;
|
||||
},
|
||||
};
|
||||
const identityApi: IdentityApi = {
|
||||
getUserId() {
|
||||
return 'jane-fonda';
|
||||
},
|
||||
getProfile() {
|
||||
return { email: 'jane-fonda@spotify.com' };
|
||||
},
|
||||
async getIdToken() {
|
||||
return Promise.resolve('fake-id-token');
|
||||
},
|
||||
async signOut() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
describe('CatalogClientWrapper', () => {
|
||||
let client: CatalogClientWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
MockedCatalogClient.mockClear();
|
||||
client = new CatalogClientWrapper({
|
||||
client: new MockedCatalogClient({ discoveryApi }),
|
||||
identityApi,
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEntities', () => {
|
||||
it('injects authorization token', async () => {
|
||||
expect.assertions(2);
|
||||
await client.getEntities();
|
||||
const getEntities = MockedCatalogClient.mock.instances[0].getEntities;
|
||||
expect(getEntities).toHaveBeenCalledWith('fake-id-token', undefined);
|
||||
expect(getEntities).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, EntityName, Location } from '@backstage/catalog-model';
|
||||
import {
|
||||
AddLocationRequest,
|
||||
AddLocationResponse,
|
||||
CatalogApi,
|
||||
CatalogEntitiesRequest,
|
||||
CatalogListResponse,
|
||||
CatalogClient,
|
||||
} from '@backstage/catalog-client';
|
||||
import { IdentityApi } from '@backstage/core';
|
||||
|
||||
export class CatalogClientWrapper implements CatalogApi {
|
||||
private readonly identityApi: IdentityApi;
|
||||
private readonly client: CatalogClient;
|
||||
|
||||
constructor(options: { client: CatalogClient; identityApi: IdentityApi }) {
|
||||
this.client = options.client;
|
||||
this.identityApi = options.identityApi;
|
||||
}
|
||||
|
||||
async getLocationById(id: String): Promise<Location | undefined> {
|
||||
const token = await this.identityApi.getIdToken();
|
||||
return await this.client.getLocationById(token, id);
|
||||
}
|
||||
|
||||
async getEntities(
|
||||
request?: CatalogEntitiesRequest,
|
||||
): Promise<CatalogListResponse<Entity>> {
|
||||
const token = await this.identityApi.getIdToken();
|
||||
return await this.client.getEntities(token, request);
|
||||
}
|
||||
|
||||
async getEntityByName(compoundName: EntityName): Promise<Entity | undefined> {
|
||||
const token = await this.identityApi.getIdToken();
|
||||
return await this.client.getEntityByName(token, compoundName);
|
||||
}
|
||||
|
||||
async addLocation(request: AddLocationRequest): Promise<AddLocationResponse> {
|
||||
const token = await this.identityApi.getIdToken();
|
||||
return await this.client.addLocation(token, request);
|
||||
}
|
||||
|
||||
async getLocationByEntity(entity: Entity): Promise<Location | undefined> {
|
||||
const token = await this.identityApi.getIdToken();
|
||||
return await this.client.getLocationByEntity(token, entity);
|
||||
}
|
||||
|
||||
async removeEntityByUid(uid: string): Promise<void> {
|
||||
const token = await this.identityApi.getIdToken();
|
||||
return await this.client.removeEntityByUid(token, uid);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
catalogRouteRef,
|
||||
entityRouteRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { catalogRouteRef, entityRouteRef } from './routes';
|
||||
import { CatalogClientWrapper } from './CatalogClientWrapper';
|
||||
|
||||
export const catalogPlugin = createPlugin({
|
||||
id: 'catalog',
|
||||
@@ -35,7 +37,10 @@ export const catalogPlugin = createPlugin({
|
||||
api: catalogApiRef,
|
||||
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
|
||||
factory: ({ discoveryApi, identityApi }) =>
|
||||
new CatalogClient({ discoveryApi, identityApi }),
|
||||
new CatalogClientWrapper({
|
||||
client: new CatalogClient({ discoveryApi }),
|
||||
identityApi,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
routes: {
|
||||
|
||||
Reference in New Issue
Block a user