address comments

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2021-12-14 14:11:30 +01:00
parent afb7f31840
commit 3fa31ec84a
22 changed files with 444 additions and 235 deletions
+25
View File
@@ -0,0 +1,25 @@
---
'@backstage/plugin-catalog': patch
---
Deprecated the `CatalogClientWrapper` class.
The default implementation of `catalogApiRef` that this plugin exposes, is now powered by the new `fetchApiRef`. The default implementation of _that_ API, in turn, has the ability to inject the user's Backstage token in requests in a similar manner to what the deprecated `CatalogClientWrapper` used to do. The latter has therefore been taken out of the default catalog API implementation.
If you use a custom `fetchApiRef` implementation that does NOT issue tokens, or use a custom `catalogApiRef` implementation which does NOT use the default `fetchApiRef`, you can still for some time wrap your catalog API in this class to get back the old behavior:
```ts
// Add this to your packages/app/src/plugins.ts if you want to get back the old
// catalog client behavior:
createApiFactory({
api: catalogApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) =>
new CatalogClientWrapper({
client: new CatalogClient({ discoveryApi }),
identityApi,
}),
}),
```
But do consider migrating to making use of the `fetchApiRef` as soon as convenient, since the wrapper class will be removed in a future release.
+17 -15
View File
@@ -34,9 +34,8 @@ import {
OneLoginAuth,
UnhandledErrorForwarder,
AtlassianAuth,
FetchApiBuilder,
IdentityAwareFetchMiddleware,
BackstageProtocolResolverFetchMiddleware,
createFetchApi,
FetchMiddlewares,
} from '@backstage/core-app-api';
import {
@@ -99,20 +98,23 @@ export const apis = [
}),
createApiFactory({
api: fetchApiRef,
deps: { identityApi: identityApiRef, discoveryApi: discoveryApiRef },
factory: ({ identityApi, discoveryApi }) => {
return FetchApiBuilder.create()
.with(
new IdentityAwareFetchMiddleware(() => {
return identityApi.getCredentials().then(r => r.token);
deps: {
configApi: configApiRef,
identityApi: identityApiRef,
discoveryApi: discoveryApiRef,
},
factory: ({ configApi, identityApi, discoveryApi }) => {
return createFetchApi({
middleware: [
FetchMiddlewares.resolvePluginProtocol({
discoveryApi,
}),
)
.with(
new BackstageProtocolResolverFetchMiddleware(plugin => {
return discoveryApi.getBaseUrl(plugin);
FetchMiddlewares.injectIdentityAuth({
identityApi,
config: configApi,
}),
)
.build();
],
});
},
}),
createApiFactory({
+1 -2
View File
@@ -5,7 +5,6 @@
```ts
import { Entity } from '@backstage/catalog-model';
import { EntityName } from '@backstage/catalog-model';
import { default as fetch_2 } from 'cross-fetch';
import { Location as Location_2 } from '@backstage/catalog-model';
// @public
@@ -159,6 +158,6 @@ export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE =
// @public
export type FetchApi = {
fetch: typeof fetch_2;
fetch: typeof fetch;
};
```
@@ -14,8 +14,6 @@
* limitations under the License.
*/
import fetch from 'cross-fetch';
/**
* This is a copy of FetchApi, to avoid importing core-plugin-api.
*
+22 -28
View File
@@ -24,9 +24,9 @@ import { BackstageIdentityApi } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { bitbucketAuthApiRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { Config } from '@backstage/config';
import { ConfigReader } from '@backstage/config';
import { createApp as createApp_2 } from '@backstage/app-defaults';
import crossFetch from 'cross-fetch';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { ErrorApi } from '@backstage/core-plugin-api';
import { ErrorApiError } from '@backstage/core-plugin-api';
@@ -290,14 +290,6 @@ export type BackstagePluginWithAnyOutput = Omit<
)[];
};
// @public
export class BackstageProtocolResolverFetchMiddleware
implements FetchMiddleware
{
constructor(discovery: (pluginId: string) => Promise<string>);
apply(next: FetchFunction): FetchFunction;
}
// @public
export class BitbucketAuth {
// (undocumented)
@@ -328,6 +320,12 @@ export function createApp(
options?: Parameters<typeof createApp_2>[0],
): BackstageApp & AppContext;
// @public
export function createFetchApi(options: {
baseImplementation?: typeof fetch | undefined;
middleware?: FetchMiddleware | FetchMiddleware[] | undefined;
}): FetchApi;
// @public
export function createSpecializedApp(options: AppOptions): BackstageApp;
@@ -380,21 +378,24 @@ export type FeatureFlaggedProps = {
);
// @public
export class FetchApiBuilder {
// (undocumented)
build(): FetchApi;
// (undocumented)
static create(): FetchApiBuilder;
// (undocumented)
with(middleware: FetchMiddleware): FetchApiBuilder;
export interface FetchMiddleware {
apply(next: typeof fetch): typeof fetch;
}
// @public
export type FetchFunction = typeof crossFetch;
// @public
export interface FetchMiddleware {
apply(next: FetchFunction): FetchFunction;
export class FetchMiddlewares {
static injectIdentityAuth(options: {
identityApi: IdentityApi;
config?: Config;
urlPrefixAllowlist?: string[];
header?: {
name: string;
value: (backstageToken: string) => string;
};
}): FetchMiddleware;
static resolvePluginProtocol(options: {
discoveryApi: DiscoveryApi;
}): FetchMiddleware;
}
// @public
@@ -454,13 +455,6 @@ export class GoogleAuth {
static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T;
}
// @public
export class IdentityAwareFetchMiddleware implements FetchMiddleware {
constructor(tokenFunction: () => Promise<string | undefined>);
apply(next: FetchFunction): FetchFunction;
setHeaderName(name: string): IdentityAwareFetchMiddleware;
}
// @public
export class LocalStorageFeatureFlags implements FeatureFlagsApi {
// (undocumented)
-1
View File
@@ -39,7 +39,6 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@types/prop-types": "^15.7.3",
"cross-fetch": "^3.0.6",
"prop-types": "^15.7.2",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^17.2.4",
@@ -0,0 +1,76 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { Config } from '@backstage/config';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware';
import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware';
import { FetchMiddleware } from './types';
/**
* A collection of common middlewares for the FetchApi.
*
* @public
*/
export class FetchMiddlewares {
/**
* Handles translation from `plugin://` URLs to concrete http(s) URLs based on
* the discovery API.
*
* @remarks
*
* If the request is for `plugin://catalog/entities?filter=x=y`, the discovery
* API will be queried for `'catalog'`. If it returned
* `https://backstage.example.net/api/catalog`, the resulting query would be
* `https://backstage.example.net/api/catalog/entities?filter=x=y`.
*
* If the incoming URL protocol was not `plugin`, the request is just passed
* through verbatim to the underlying implementation.
*/
static resolvePluginProtocol(options: {
discoveryApi: DiscoveryApi;
}): FetchMiddleware {
return new PluginProtocolResolverFetchMiddleware(options.discoveryApi);
}
/**
* Injects a Backstage token header when the user is signed in.
*
* @remarks
*
* Per default, an `Authorization: Bearer <token>` is generated. This can be
* customized using the `header` option.
*
* The header injection only happens on allowlisted URLs. Per default, if the
* `config` option is passed in, the `backend.baseUrl` is allowlisted, unless
* the `urlPrefixAllowlist` option is passed in, in which case it takes
* precedence. If you pass in neither config nor an allowlist, the middleware
* will have no effect.
*/
static injectIdentityAuth(options: {
identityApi: IdentityApi;
config?: Config;
urlPrefixAllowlist?: string[];
header?: {
name: string;
value: (backstageToken: string) => string;
};
}): FetchMiddleware {
return IdentityAuthInjectorFetchMiddleware.create(options);
}
private constructor() {}
}
@@ -0,0 +1,153 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { ConfigReader } from '@backstage/config';
import { IdentityApi } from '@backstage/core-plugin-api';
import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware';
describe('IdentityAuthInjectorFetchMiddleware', () => {
it('creates using defaults', async () => {
const middleware = IdentityAuthInjectorFetchMiddleware.create({
identityApi: undefined as any,
});
expect(middleware.urlPrefixAllowlist).toEqual([]);
expect(middleware.headerName).toEqual('authorization');
expect(middleware.headerValue('t')).toEqual('Bearer t');
});
it('creates using config', async () => {
const middleware = IdentityAuthInjectorFetchMiddleware.create({
identityApi: undefined as any,
config: new ConfigReader({
backend: { baseUrl: 'https://example.com/api' },
}),
header: { name: 'auth', value: t => `${t}!` },
});
expect(middleware.urlPrefixAllowlist).toEqual(['https://example.com/api']);
expect(middleware.headerName).toEqual('auth');
expect(middleware.headerValue('t')).toEqual('t!');
});
it('creates using explicit allowlist', async () => {
const middleware = IdentityAuthInjectorFetchMiddleware.create({
identityApi: undefined as any,
config: new ConfigReader({
backend: { baseUrl: 'https://example.com/api' },
}),
urlPrefixAllowlist: ['https://a.com', 'http://b.com:8080/'],
});
expect(middleware.urlPrefixAllowlist).toEqual([
'https://a.com',
'http://b.com:8080',
]);
});
it('injects the header only when a token is available', async () => {
const tokenFunction = jest.fn();
const identityApi = {
getCredentials: tokenFunction,
} as unknown as IdentityApi;
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
['https://example.com'],
'Authorization',
token => `Bearer ${token}`,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
// No token available
tokenFunction.mockResolvedValueOnce({ token: undefined });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]);
// Supply a token, header gets added
tokenFunction.mockResolvedValueOnce({ token: 'token' });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([
['authorization', 'Bearer token'],
]);
// Token no longer available
tokenFunction.mockResolvedValueOnce({ token: undefined });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]);
});
it('does not overwrite an existing header with the same name', async () => {
const identityApi = {
getCredentials: () => ({ token: 'token' }),
} as unknown as IdentityApi;
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
['https://example.com'],
'Authorization',
token => `Bearer ${token}`,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
// No token available
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([
['authorization', 'Bearer token'],
]);
// Supply a token, header gets added
await outer(
new Request('https://example.com', {
headers: { authorization: 'do-not-clobber' },
}),
);
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([
['authorization', 'do-not-clobber'],
]);
});
it('does not affect requests outside the allowlist', async () => {
const identityApi = {
getCredentials: () => ({ token: 'token' }),
} as unknown as IdentityApi;
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
['https://example.com:8080/root'],
'Authorization',
token => `Bearer ${token}`,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
await outer(new Request('https://example.com:8080/root'));
await outer(new Request('https://example.com:8080/root/sub'));
await outer(new Request('https://example.com:8080/root2'));
await outer(new Request('https://example.com/root'));
await outer(new Request('http://example.com:8080/root'));
await outer(new Request('https://example.com/root'));
const no: string[][] = [];
const yes: string[][] = [['authorization', 'Bearer token']];
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual(yes);
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual(yes);
expect([...inner.mock.calls[2][0].headers.entries()]).toEqual(no);
expect([...inner.mock.calls[3][0].headers.entries()]).toEqual(no);
expect([...inner.mock.calls[4][0].headers.entries()]).toEqual(no);
expect([...inner.mock.calls[5][0].headers.entries()]).toEqual(no);
});
});
@@ -0,0 +1,82 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { Config } from '@backstage/config';
import { IdentityApi } from '@backstage/core-plugin-api';
import { FetchMiddleware } from './types';
/**
* A fetch middleware, which injects a Backstage token header when the user is
* signed in.
*/
export class IdentityAuthInjectorFetchMiddleware implements FetchMiddleware {
static create(options: {
identityApi: IdentityApi;
config?: Config;
urlPrefixAllowlist?: string[];
header?: {
name: string;
value: (backstageToken: string) => string;
};
}): IdentityAuthInjectorFetchMiddleware {
const allowlist: string[] = [];
if (options.urlPrefixAllowlist) {
allowlist.push(...options.urlPrefixAllowlist);
} else if (options.config) {
allowlist.push(options.config.getString('backend.baseUrl'));
}
const headerName = options.header?.name || 'authorization';
const headerValue = options.header?.value || (token => `Bearer ${token}`);
return new IdentityAuthInjectorFetchMiddleware(
options.identityApi,
allowlist.map(prefix => prefix.replace(/\/$/, '')),
headerName,
headerValue,
);
}
constructor(
public readonly identityApi: IdentityApi,
public readonly urlPrefixAllowlist: string[],
public readonly headerName: string,
public readonly headerValue: (pluginId: string) => string,
) {}
apply(next: typeof fetch): typeof fetch {
return async (input, init) => {
// Skip this middleware if the header already exists, or if the URL
// doesn't match any of the allowlist items, or if there was no token
const request = new Request(input, init);
const { token } = await this.identityApi.getCredentials();
if (
request.headers.get(this.headerName) ||
!this.urlPrefixAllowlist.some(
prefix =>
request.url === prefix || request.url.startsWith(`${prefix}/`),
) ||
typeof token !== 'string' ||
!token
) {
return next(input, init);
}
request.headers.set(this.headerName, this.headerValue(token));
return next(request);
};
}
}
@@ -1,43 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { IdentityAwareFetchMiddleware } from './IdentityAwareFetchMiddleware';
describe('IdentityAwareFetchMiddleware', () => {
it('injects the header only when a token is available', async () => {
const tokenFunction = jest.fn();
const middleware = new IdentityAwareFetchMiddleware(tokenFunction);
const inner = jest.fn();
const outer = middleware.apply(inner);
// No token available
tokenFunction.mockResolvedValueOnce(undefined);
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]);
// Supply a token, header gets added
tokenFunction.mockResolvedValueOnce('token');
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([
['backstage-token', 'token'],
]);
// Token no longer available
tokenFunction.mockResolvedValueOnce(undefined);
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]);
});
});
@@ -1,59 +0,0 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { FetchFunction, FetchMiddleware } from './types';
const DEFAULT_HEADER_NAME = 'backstage-token';
/**
* A fetch middleware, which injects a Backstage token header when the user is
* signed in.
*
* @public
*/
export class IdentityAwareFetchMiddleware implements FetchMiddleware {
private headerName: string;
private tokenFunction: () => Promise<string | undefined>;
constructor(tokenFunction: () => Promise<string | undefined>) {
this.headerName = DEFAULT_HEADER_NAME;
this.tokenFunction = tokenFunction;
}
/**
* {@inheritdoc FetchMiddleware.apply}
*/
apply(next: FetchFunction): FetchFunction {
return async (input, init) => {
const token = await this.tokenFunction();
if (typeof token !== 'string') {
return next(input, init);
}
const request = new Request(input, init);
request.headers.set(this.headerName, token);
return next(request);
};
}
/**
* Changes the header name from the default value to a custom one.
*/
setHeaderName(name: string): IdentityAwareFetchMiddleware {
this.headerName = name;
return this;
}
}
@@ -14,14 +14,18 @@
* limitations under the License.
*/
import { BackstageProtocolResolverFetchMiddleware } from './BackstageProtocolResolverFetchMiddleware';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware';
describe('BackstageProtocolResolverFetchMiddleware', () => {
describe('PluginProtocolResolverFetchMiddleware', () => {
it.each([['https://passthrough.com/a']])(
'passes through regular URLs, %p',
async (url: string) => {
async url => {
const resolve = jest.fn();
const middleware = new BackstageProtocolResolverFetchMiddleware(resolve);
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
const middleware = new PluginProtocolResolverFetchMiddleware(
discoveryApi,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
@@ -33,38 +37,38 @@ describe('BackstageProtocolResolverFetchMiddleware', () => {
it.each([
[
'backstage://my-plugin/sub/path',
'plugin://my-plugin/sub/path',
'my-plugin',
'https://real.com/base',
'https://real.com/base/sub/path',
],
[
'backstage://my-plugin/sub/path/',
'plugin://my-plugin/sub/path/',
'my-plugin',
'https://real.com/base/',
'https://real.com/base/sub/path/',
],
['backstage://x', 'x', 'http://real.com:8080', 'http://real.com:8080'],
['plugin://x', 'x', 'http://real.com:8080', 'http://real.com:8080'],
[
'backstage://x/a/b?c=d&e=f#g',
'plugin://x/a/b?c=d&e=f#g',
'x',
'https://real.com/base',
'https://real.com/base/a/b?c=d&e=f#g',
],
[
'backstage://x?c=d&e=f#g',
'plugin://x?c=d&e=f#g',
'x',
'https://real.com:8080/base',
'https://real.com:8080/base?c=d&e=f#g',
],
[
'backstage://username:password@x?c=d&e=f#g',
'plugin://username:password@x?c=d&e=f#g',
'x',
'https://real.com:8080/base',
'https://username:password@real.com:8080/base?c=d&e=f#g',
],
[
'backstage://x?c=d&e=f#g',
'plugin://x?c=d&e=f#g',
'x',
'https://username:password@real.com:8080/base',
'https://username:password@real.com:8080/base?c=d&e=f#g',
@@ -73,7 +77,10 @@ describe('BackstageProtocolResolverFetchMiddleware', () => {
'resolves backstage URLs, %p',
async (original, host, resolved, result) => {
const resolve = jest.fn();
const middleware = new BackstageProtocolResolverFetchMiddleware(resolve);
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
const middleware = new PluginProtocolResolverFetchMiddleware(
discoveryApi,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { FetchFunction, FetchMiddleware } from './types';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { FetchMiddleware } from './types';
function join(left: string, right: string): string {
if (!right || right === '/') {
@@ -25,25 +26,16 @@ function join(left: string, right: string): string {
}
/**
* Handles translation from backstage://some-plugin-id/<path> to concrete
* http(s) URLs.
*
* @public
* Handles translation from plugin://some-plugin-id/<path> to concrete http(s)
* URLs.
*/
export class BackstageProtocolResolverFetchMiddleware
implements FetchMiddleware
{
constructor(
private readonly discovery: (pluginId: string) => Promise<string>,
) {}
export class PluginProtocolResolverFetchMiddleware implements FetchMiddleware {
constructor(private readonly discoveryApi: DiscoveryApi) {}
/**
* {@inheritdoc FetchMiddleware.apply}
*/
apply(next: FetchFunction): FetchFunction {
apply(next: typeof fetch): typeof fetch {
return async (input, init) => {
const request = new Request(input, init);
const prefix = 'backstage://';
const prefix = 'plugin://';
if (!request.url.startsWith(prefix)) {
return next(input, init);
@@ -55,7 +47,7 @@ export class BackstageProtocolResolverFetchMiddleware
`http://${request.url.substring(prefix.length)}`,
);
let base = await this.discovery(hostname);
let base = await this.discoveryApi.getBaseUrl(hostname);
if (username || password) {
const baseUrl = new URL(base);
const authority = `${username}${password ? `:${password}` : ''}@`;
@@ -15,41 +15,32 @@
*/
import { FetchApi } from '@backstage/core-plugin-api';
import crossFetch from 'cross-fetch';
import { FetchFunction, FetchMiddleware } from './types';
import { FetchMiddleware } from './types';
/**
* Builds a fetch API, based on the builtin fetch wrapped by a set of optional
* middleware implementations that add behaviors.
*
* @remarks
*
* The middleware are applied in reverse order, i.e. the last one will be
* "closest" to the base implementation. Passing in `[M1, M2, M3]` effectively
* leads to `M1(M2(M3(baseImplementation)))`.
*
* @public
*/
export class FetchApiBuilder {
static create(): FetchApiBuilder {
return new FetchApiBuilder(crossFetch, []);
export function createFetchApi(options: {
baseImplementation?: typeof fetch | undefined;
middleware?: FetchMiddleware | FetchMiddleware[] | undefined;
}): FetchApi {
let result = options.baseImplementation || global.fetch;
const middleware = [options.middleware ?? []].flat().reverse();
for (const m of middleware) {
result = m.apply(result);
}
private constructor(
private readonly implementation: FetchFunction,
private readonly middleware: FetchMiddleware[],
) {}
with(middleware: FetchMiddleware): FetchApiBuilder {
return new FetchApiBuilder(this.implementation, [
...this.middleware,
middleware,
]);
}
build(): FetchApi {
let result = this.implementation;
for (const m of this.middleware) {
result = m.apply(result);
}
return {
fetch: result,
};
}
return {
fetch: result,
};
}
@@ -14,7 +14,6 @@
* limitations under the License.
*/
export { BackstageProtocolResolverFetchMiddleware } from './BackstageProtocolResolverFetchMiddleware';
export { FetchApiBuilder } from './FetchApiBuilder';
export { IdentityAwareFetchMiddleware } from './IdentityAwareFetchMiddleware';
export type { FetchFunction, FetchMiddleware } from './types';
export { createFetchApi } from './createFetchApi';
export { FetchMiddlewares } from './FetchMiddlewares';
export type { FetchMiddleware } from './types';
@@ -14,15 +14,6 @@
* limitations under the License.
*/
import crossFetch from 'cross-fetch';
/**
* The type of a fetch call.
*
* @public
*/
export type FetchFunction = typeof crossFetch;
/**
* A middleware that modifies the behavior of an ongoing fetch.
*
@@ -35,5 +26,5 @@ export interface FetchMiddleware {
* @param next - The next, inner, implementation, that this middleware shall
* call out to as part of the request cycle.
*/
apply(next: FetchFunction): FetchFunction;
apply(next: typeof fetch): typeof fetch;
}
+1 -2
View File
@@ -9,7 +9,6 @@ import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/core-plugin-api
import { BackstageTheme } from '@backstage/theme';
import { ComponentType } from 'react';
import { Config } from '@backstage/config';
import { default as fetch_2 } from 'cross-fetch';
import { IconComponent as IconComponent_2 } from '@backstage/core-plugin-api';
import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api';
import { Observable as Observable_2 } from '@backstage/types';
@@ -508,7 +507,7 @@ export enum FeatureFlagState {
// @public
export type FetchApi = {
fetch: typeof fetch_2;
fetch: typeof fetch;
};
// @public
+1 -1
View File
@@ -34,7 +34,6 @@
"@backstage/types": "^0.1.1",
"@backstage/version-bridge": "^0.1.1",
"@material-ui/core": "^4.12.2",
"cross-fetch": "^3.0.6",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react-router-dom": "6.0.0-beta.0",
@@ -57,6 +56,7 @@
"@types/node": "^14.14.32",
"@types/prop-types": "^15.7.3",
"@types/zen-observable": "^0.8.0",
"cross-fetch": "^3.0.6",
"msw": "^0.35.0"
},
"files": [
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import fetch from 'cross-fetch';
import { ApiRef, createApiRef } from '../system';
/**
+1 -1
View File
@@ -66,7 +66,7 @@ export type BackstageOverrides = Overrides & {
// Warning: (ae-missing-release-tag) "CatalogClientWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
// @public @deprecated
export class CatalogClientWrapper implements CatalogApi {
constructor(options: { client: CatalogClient; identityApi: IdentityApi });
// (undocumented)
@@ -30,6 +30,13 @@ import { IdentityApi } from '@backstage/core-plugin-api';
/**
* CatalogClient wrapper that injects identity token for all requests
*
* @deprecated The default catalog client now uses the `fetchApiRef`
* implementation, which in turn by default issues tokens just the same as this
* class used to assist in doing. If you use a custom `fetchApiRef`
* implementation that does NOT issue tokens, or use a custom `catalogApiRef`
* implementation which does not use the default `fetchApiRef`, you can wrap
* your catalog API in this class to get back the old behavior.
*/
export class CatalogClientWrapper implements CatalogApi {
private readonly identityApi: IdentityApi;
+7 -9
View File
@@ -22,7 +22,6 @@ import {
entityRouteRef,
starredEntitiesApiRef,
} from '@backstage/plugin-catalog-react';
import { CatalogClientWrapper } from './CatalogClientWrapper';
import { createComponentRouteRef, viewTechDocRouteRef } from './routes';
import {
createApiFactory,
@@ -30,7 +29,7 @@ import {
createPlugin,
createRoutableExtension,
discoveryApiRef,
identityApiRef,
fetchApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
@@ -39,14 +38,13 @@ export const catalogPlugin = createPlugin({
apis: [
createApiFactory({
api: catalogApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) =>
new CatalogClientWrapper({
client: new CatalogClient({ discoveryApi }),
identityApi,
}),
deps: {
discoveryApi: discoveryApiRef,
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi, fetchApi }) =>
new CatalogClient({ discoveryApi, fetchApi }),
}),
createApiFactory({
api: starredEntitiesApiRef,
deps: { storageApi: storageApiRef },