Merge pull request #6399 from backstage/freben/fetch-api

Add `fetchApiRef` which implements fetch, plus Backstage token header when available
This commit is contained in:
Fredrik Adelöw
2021-12-16 11:30:37 +01:00
committed by GitHub
25 changed files with 755 additions and 19 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/app-defaults': patch
'@backstage/core-app-api': patch
'@backstage/core-plugin-api': patch
---
Add `FetchApi` and related `fetchApiRef` which implement fetch, with an added Backstage token header when available.
+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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/catalog-client': patch
---
Add the ability to supply a custom `fetchApi`. In the default frontend app setup, this will use the `fetchApiRef` implementation.
@@ -34,6 +34,8 @@ import {
OneLoginAuth,
UnhandledErrorForwarder,
AtlassianAuth,
createFetchApi,
FetchMiddlewares,
} from '@backstage/core-app-api';
import {
@@ -42,6 +44,8 @@ import {
analyticsApiRef,
errorApiRef,
discoveryApiRef,
fetchApiRef,
identityApiRef,
oauthRequestApiRef,
googleAuthApiRef,
githubAuthApiRef,
@@ -92,6 +96,27 @@ export const apis = [
deps: { errorApi: errorApiRef },
factory: ({ errorApi }) => WebStorage.create({ errorApi }),
}),
createApiFactory({
api: fetchApiRef,
deps: {
configApi: configApiRef,
identityApi: identityApiRef,
discoveryApi: discoveryApiRef,
},
factory: ({ configApi, identityApi, discoveryApi }) => {
return createFetchApi({
middleware: [
FetchMiddlewares.resolvePluginProtocol({
discoveryApi,
}),
FetchMiddlewares.injectIdentityAuth({
identityApi,
config: configApi,
}),
],
});
},
}),
createApiFactory({
api: oauthRequestApiRef,
deps: {},
+6 -1
View File
@@ -71,7 +71,7 @@ export interface CatalogApi {
// @public
export class CatalogClient implements CatalogApi {
constructor(options: { discoveryApi: DiscoveryApi });
constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi });
addLocation(
{ type, target, dryRun, presence }: AddLocationRequest,
options?: CatalogRequestOptions,
@@ -155,4 +155,9 @@ export type DiscoveryApi = {
// @public
export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE =
'backstage.io/catalog-processing';
// @public
export type FetchApi = {
fetch: typeof fetch;
};
```
+10 -7
View File
@@ -25,7 +25,7 @@ import {
stringifyLocationReference,
} from '@backstage/catalog-model';
import { ResponseError } from '@backstage/errors';
import fetch from 'cross-fetch';
import crossFetch from 'cross-fetch';
import {
CATALOG_FILTER_EXISTS,
AddLocationRequest,
@@ -38,6 +38,7 @@ import {
CatalogEntityAncestorsResponse,
} from './types/api';
import { DiscoveryApi } from './types/discovery';
import { FetchApi } from './types/fetch';
/**
* A frontend and backend compatible client for communicating with the Backstage Catalog.
@@ -46,9 +47,11 @@ import { DiscoveryApi } from './types/discovery';
* */
export class CatalogClient implements CatalogApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
constructor(options: { discoveryApi: DiscoveryApi }) {
constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi || { fetch: crossFetch };
}
/**
@@ -206,7 +209,7 @@ export class CatalogClient implements CatalogApi {
* @public
*/
async refreshEntity(entityRef: string, options?: CatalogRequestOptions) {
const response = await fetch(
const response = await this.fetchApi.fetch(
`${await this.discoveryApi.getBaseUrl('catalog')}/refresh`,
{
headers: {
@@ -237,7 +240,7 @@ export class CatalogClient implements CatalogApi {
{ type = 'url', target, dryRun, presence }: AddLocationRequest,
options?: CatalogRequestOptions,
): Promise<AddLocationResponse> {
const response = await fetch(
const response = await this.fetchApi.fetch(
`${await this.discoveryApi.getBaseUrl('catalog')}/locations${
dryRun ? '?dryRun=true' : ''
}`,
@@ -376,7 +379,7 @@ export class CatalogClient implements CatalogApi {
const headers: Record<string, string> = options?.token
? { Authorization: `Bearer ${options.token}` }
: {};
const response = await fetch(url, { method, headers });
const response = await this.fetchApi.fetch(url, { method, headers });
if (!response.ok) {
throw await ResponseError.fromResponse(response);
@@ -392,7 +395,7 @@ export class CatalogClient implements CatalogApi {
const headers: Record<string, string> = options?.token
? { Authorization: `Bearer ${options.token}` }
: {};
const response = await fetch(url, { method, headers });
const response = await this.fetchApi.fetch(url, { method, headers });
if (!response.ok) {
throw await ResponseError.fromResponse(response);
@@ -410,7 +413,7 @@ export class CatalogClient implements CatalogApi {
const headers: Record<string, string> = options?.token
? { Authorization: `Bearer ${options.token}` }
: {};
const response = await fetch(url, { method, headers });
const response = await this.fetchApi.fetch(url, { method, headers });
if (!response.ok) {
if (response.status === 404) {
@@ -15,7 +15,7 @@
*/
/**
* This is a copy of the core DiscoveryApi, to avoid importing core.
* This is a copy of the DiscoveryApi, to avoid importing core-plugin-api.
*
* @public
*/
@@ -0,0 +1,24 @@
/*
* 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.
*/
/**
* This is a copy of FetchApi, to avoid importing core-plugin-api.
*
* @public
*/
export type FetchApi = {
fetch: typeof fetch;
};
@@ -25,5 +25,6 @@ export type {
CatalogEntityAncestorsResponse,
} from './api';
export type { DiscoveryApi } from './discovery';
export type { FetchApi } from './fetch';
export { CATALOG_FILTER_EXISTS } from './api';
export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status';
+29
View File
@@ -24,6 +24,7 @@ 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 { DiscoveryApi } from '@backstage/core-plugin-api';
@@ -34,6 +35,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api';
import { FeatureFlag } from '@backstage/core-plugin-api';
import { FeatureFlagsApi } from '@backstage/core-plugin-api';
import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
import { googleAuthApiRef } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
@@ -318,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;
@@ -369,6 +377,27 @@ export type FeatureFlaggedProps = {
}
);
// @public
export interface FetchMiddleware {
apply(next: typeof fetch): typeof fetch;
}
// @public
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
export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null;
@@ -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);
};
}
}
@@ -0,0 +1,93 @@
/*
* 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 { DiscoveryApi } from '@backstage/core-plugin-api';
import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware';
describe('PluginProtocolResolverFetchMiddleware', () => {
it.each([['https://passthrough.com/a']])(
'passes through regular URLs, %p',
async url => {
const resolve = jest.fn();
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
const middleware = new PluginProtocolResolverFetchMiddleware(
discoveryApi,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
await outer(url);
expect(inner.mock.calls[0][0]).toBe(url);
expect(resolve).not.toBeCalled();
},
);
it.each([
[
'plugin://my-plugin/sub/path',
'my-plugin',
'https://real.com/base',
'https://real.com/base/sub/path',
],
[
'plugin://my-plugin/sub/path/',
'my-plugin',
'https://real.com/base/',
'https://real.com/base/sub/path/',
],
['plugin://x', 'x', 'http://real.com:8080', 'http://real.com:8080'],
[
'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',
],
[
'plugin://x?c=d&e=f#g',
'x',
'https://real.com:8080/base',
'https://real.com:8080/base?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',
],
[
'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',
],
])(
'resolves backstage URLs, %p',
async (original, host, resolved, result) => {
const resolve = jest.fn();
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
const middleware = new PluginProtocolResolverFetchMiddleware(
discoveryApi,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
resolve.mockResolvedValueOnce(resolved);
await outer(original);
expect(inner.mock.calls[0][0]).toBe(result);
expect(resolve).toHaveBeenLastCalledWith(host);
},
);
});
@@ -0,0 +1,61 @@
/*
* 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 { DiscoveryApi } from '@backstage/core-plugin-api';
import { FetchMiddleware } from './types';
function join(left: string, right: string): string {
if (!right || right === '/') {
return left;
}
return `${left.replace(/\/$/, '')}/${right.replace(/^\//, '')}`;
}
/**
* Handles translation from plugin://some-plugin-id/<path> to concrete http(s)
* URLs.
*/
export class PluginProtocolResolverFetchMiddleware implements FetchMiddleware {
constructor(private readonly discoveryApi: DiscoveryApi) {}
apply(next: typeof fetch): typeof fetch {
return async (input, init) => {
const request = new Request(input, init);
const prefix = 'plugin://';
if (!request.url.startsWith(prefix)) {
return next(input, init);
}
// Switch to a known protocol, since browser URL parsing misbehaves wildly
// on foreign protocols
const { hostname, pathname, search, hash, username, password } = new URL(
`http://${request.url.substring(prefix.length)}`,
);
let base = await this.discoveryApi.getBaseUrl(hostname);
if (username || password) {
const baseUrl = new URL(base);
const authority = `${username}${password ? `:${password}` : ''}@`;
base = `${baseUrl.protocol}//${authority}${baseUrl.host}${baseUrl.pathname}`;
}
const target = `${join(base, pathname)}${search}${hash}`;
return next(target, request);
};
}
}
@@ -0,0 +1,46 @@
/*
* 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 { FetchApi } from '@backstage/core-plugin-api';
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 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);
}
return {
fetch: result,
};
}
@@ -0,0 +1,19 @@
/*
* 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.
*/
export { createFetchApi } from './createFetchApi';
export { FetchMiddlewares } from './FetchMiddlewares';
export type { FetchMiddleware } from './types';
@@ -0,0 +1,30 @@
/*
* 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.
*/
/**
* A middleware that modifies the behavior of an ongoing fetch.
*
* @public
*/
export interface FetchMiddleware {
/**
* Applies this middleware to an inner implementation.
*
* @param next - The next, inner, implementation, that this middleware shall
* call out to as part of the request cycle.
*/
apply(next: typeof fetch): typeof fetch;
}
@@ -27,5 +27,6 @@ export * from './ConfigApi';
export * from './DiscoveryApi';
export * from './ErrorApi';
export * from './FeatureFlagsApi';
export * from './FetchApi';
export * from './OAuthRequestApi';
export * from './StorageApi';
+8
View File
@@ -506,6 +506,14 @@ export enum FeatureFlagState {
None = 0,
}
// @public
export type FetchApi = {
fetch: typeof fetch;
};
// @public
export const fetchApiRef: ApiRef<FetchApi>;
// @public
export function getComponentData<T>(
node: ReactNode,
@@ -0,0 +1,37 @@
/*
* 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 { ApiRef, createApiRef } from '../system';
/**
* A wrapper for the fetch API, that has additional behaviors such as the
* ability to automatically inject auth information where necessary.
*
* @public
*/
export type FetchApi = {
fetch: typeof fetch;
};
/**
* A wrapper for the fetch API, that has additional behaviors such as the
* ability to automatically inject auth information where necessary.
*
* @public
*/
export const fetchApiRef: ApiRef<FetchApi> = createApiRef({
id: 'core.fetch',
});
@@ -29,6 +29,7 @@ export * from './ConfigApi';
export * from './DiscoveryApi';
export * from './ErrorApi';
export * from './FeatureFlagsApi';
export * from './FetchApi';
export * from './IdentityApi';
export * from './OAuthRequestApi';
export * from './StorageApi';
+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 },