Merge branch 'master' into ndudnik/github-actions-annotation
This commit is contained in:
@@ -13,6 +13,18 @@ jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432/tcp
|
||||
# needed because the postgres container does not provide a healthcheck
|
||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
@@ -44,6 +56,11 @@ jobs:
|
||||
- run: yarn tsc
|
||||
- run: yarn build
|
||||
- name: verify app and plugin creation
|
||||
env:
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }}
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
run: |
|
||||
sudo sysctl fs.inotify.max_user_watches=524288
|
||||
node ${{ github.workspace }}/packages/cli/e2e-test/cli-e2e-test.js
|
||||
|
||||
+13
-13
@@ -1,13 +1,13 @@
|
||||
| Organization | Contact | Description of Use |
|
||||
| ---------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
|
||||
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
|
||||
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
|
||||
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
|
||||
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
|
||||
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
|
||||
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
|
||||
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
|
||||
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
|
||||
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
|
||||
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D
|
||||
| Organization | Contact | Description of Use |
|
||||
| --------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| [Spotify](https://www.spotify.com) | [@stefanalund](https://github.com/stefanalund) | Main interface towards all of Spotify's infrastructure and technical documentation. |
|
||||
| [bol.com](https://www.bol.com) | [@RoyJacobs](https://github.com/RoyJacobs) | Initial work being done to unify platform tooling. |
|
||||
| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. |
|
||||
| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up |
|
||||
| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. |
|
||||
| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. |
|
||||
| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. |
|
||||
| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications |
|
||||
| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. |
|
||||
| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. |
|
||||
| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D |
|
||||
|
||||
@@ -25,3 +25,72 @@ techdocs:
|
||||
|
||||
sentry:
|
||||
organization: spotify
|
||||
|
||||
auth:
|
||||
providers:
|
||||
google:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_GOOGLE_CLIENT_ID
|
||||
clientSecret:
|
||||
$secret:
|
||||
env: AUTH_GOOGLE_CLIENT_SECRET
|
||||
github:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_GITHUB_CLIENT_ID
|
||||
clientSecret:
|
||||
$secret:
|
||||
env: AUTH_GITHUB_CLIENT_SECRET
|
||||
gitlab:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_GITLAB_CLIENT_ID
|
||||
clientSecret:
|
||||
$secret:
|
||||
env: AUTH_GITLAB_CLIENT_SECRET
|
||||
audience:
|
||||
$secret:
|
||||
env: GITLAB_BASE_URL
|
||||
# saml:
|
||||
# development:
|
||||
# entryPoint: "http://localhost:7001/"
|
||||
# issuer: "passport-saml"
|
||||
okta:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_OKTA_CLIENT_ID
|
||||
clientSecret:
|
||||
$secret:
|
||||
env: AUTH_OKTA_CLIENT_SECRET
|
||||
audience:
|
||||
$secret:
|
||||
env: AUTH_OKTA_AUDIENCE
|
||||
oauth2:
|
||||
development:
|
||||
appOrigin: "http://localhost:3000/"
|
||||
secure: false
|
||||
clientId:
|
||||
$secret:
|
||||
env: AUTH_OAUTH2_CLIENT_ID
|
||||
clientSecret:
|
||||
$secret:
|
||||
env: AUTH_OAUTH2_CLIENT_SECRET
|
||||
authorizationURL:
|
||||
$secret:
|
||||
env: AUTH_OAUTH2_AUTH_URL
|
||||
tokenURL:
|
||||
$secret:
|
||||
env: AUTH_OAUTH2_TOKEN_URL
|
||||
|
||||
+3
-1
@@ -77,7 +77,8 @@ better yet, a pull request.
|
||||
- [Figma resources](dls/figma.md)
|
||||
- API references
|
||||
- TypeScript API
|
||||
- [Utilities](api/utility-apis.md)
|
||||
- [Utility APIs](api/utility-apis.md)
|
||||
- [Utility API References](reference/utility-apis/README.md)
|
||||
- [createPlugin](reference/createPlugin.md)
|
||||
- [createPlugin-feature-flags](reference/createPlugin-feature-flags.md)
|
||||
- [createPlugin-router](reference/createPlugin-router.md)
|
||||
@@ -93,6 +94,7 @@ better yet, a pull request.
|
||||
- [ADR004 - Module Export Structure](architecture-decisions/adr004-module-export-structure.md)
|
||||
- [ADR005 - Catalog Core Entities](architecture-decisions/adr005-catalog-core-entities.md)
|
||||
- [ADR006 - Avoid React.FC and React.SFC](architecture-decisions/adr006-avoid-react-fc.md)
|
||||
- [ADR007 - Use MSW for Mocking Network Requests](architecture-decisions/adr007-use-msw-to-mock-service-requests.md)
|
||||
- [Contribute](../CONTRIBUTING.md)
|
||||
- [Support](overview/support.md)
|
||||
- [FAQ](FAQ.md)
|
||||
|
||||
@@ -19,7 +19,8 @@ during their entire life cycle.
|
||||
Each Utility API is tied to an `ApiRef` instance, which is a global singleton
|
||||
object without any additional state or functionality, its only purpose is to
|
||||
reference Utility APIs. `ApiRef`s are create using `createApiRef`, which is
|
||||
exported by `@backstage/core`. There are many predefined Utility APIs defined in
|
||||
exported by `@backstage/core`. There are many
|
||||
[predefined Utility APIs](../reference/utility-apis/README.md) defined in
|
||||
`@backstage/core`, and they're all exported with a name of the pattern
|
||||
`*ApiRef`, for example `errorApiRef`.
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# ADR007: Use MSW to mock http requests
|
||||
|
||||
## Context
|
||||
|
||||
Network request mocking can be a total pain sometimes, in all different types of
|
||||
tests, unit tests to e2e tests always have their own implementation of mocking
|
||||
these requests. There's been traction in the outer community towards using this
|
||||
library to mock network requests by using an express style declaration for
|
||||
routes. react-testing-library suggests using this library instead of mocking
|
||||
fetch directly wether this be in a browser or in node.
|
||||
|
||||
https://github.com/mswjs/msw
|
||||
|
||||
## Decision
|
||||
|
||||
Moving forward, we have decided that any `fetch` or `XMLHTTPRequest` that
|
||||
happens, should be mocked by using `msw`.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```ts
|
||||
import { setupWorker, rest } from 'msw';
|
||||
|
||||
const worker = setupWorker(
|
||||
rest.get('*/user/:userId', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.json({
|
||||
firstName: 'John',
|
||||
lastName: 'Maverick',
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// Start the Mock Service Worker
|
||||
worker.start();
|
||||
```
|
||||
|
||||
and in a more real life scenario, taken from
|
||||
[CatalogClient.test.ts](https://github.com/spotify/backstage/blob/f3245c4f8f0b6b2625c4a6d5d50161b612fb4757/plugins/catalog/src/api/CatalogClient.test.ts)
|
||||
|
||||
```ts
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.get(`${mockApiOrigin}${mockBasePath}/entities`, (_, res, ctx) => {
|
||||
return res(ctx.json(defaultResponse));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should entities from correct endpoint', async () => {
|
||||
const entities = await client.getEntities();
|
||||
expect(entities).toEqual(defaultResponse);
|
||||
});
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
- A little more code to write
|
||||
- Gradually will replace the codebase with `msw`
|
||||
+2
-1
@@ -26,7 +26,8 @@ OAuth helps in that regard.
|
||||
|
||||
The method with which frontend plugins request access to third party services is
|
||||
through [Utility APIs](../api/utility-apis.md) for each service provider. For a
|
||||
full list of providers, see [TODO](#TODO).
|
||||
full list of providers, see the
|
||||
[Utility API References](../reference/utility-apis/README.md).
|
||||
|
||||
### Identity - WIP
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# AlertApi
|
||||
|
||||
The AlertApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AlertApi.ts#L29).
|
||||
|
||||
The following Utility API implements this type: [alertApiRef](./README.md#alert)
|
||||
|
||||
## Members
|
||||
|
||||
### post()
|
||||
|
||||
Post an alert for handling by the application.
|
||||
|
||||
<pre>
|
||||
post(alert: <a href="#alertmessage">AlertMessage</a>): void
|
||||
</pre>
|
||||
|
||||
### alert\$()
|
||||
|
||||
Observe alerts posted by other parts of the application.
|
||||
|
||||
<pre>
|
||||
alert$(): <a href="#observable">Observable</a><<a href="#alertmessage">AlertMessage</a>>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### AlertMessage
|
||||
|
||||
<pre>
|
||||
export type AlertMessage = {
|
||||
message: string;
|
||||
// Severity will default to success since that is what material ui defaults the value to.
|
||||
severity?: 'success' | 'info' | 'warning' | 'error';
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AlertApi.ts#L19).
|
||||
|
||||
Referenced by: [post](#post), [alert\$](#alert).
|
||||
|
||||
### Observable
|
||||
|
||||
Observable sequence of values and errors, see TC39.
|
||||
|
||||
https://github.com/tc39/proposal-observable
|
||||
|
||||
This is used as a common return type for observable values and can be created
|
||||
using many different observable implementations, such as zen-observable or
|
||||
RxJS 5.
|
||||
|
||||
<pre>
|
||||
export type Observable<T> = {
|
||||
/**
|
||||
* Subscribes to this observable to start receiving new values.
|
||||
*/
|
||||
subscribe(observer: <a href="#observer">Observer</a><T>): <a href="#subscription">Subscription</a>;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: Error) => void,
|
||||
onComplete?: () => void,
|
||||
): <a href="#subscription">Subscription</a>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
|
||||
|
||||
Referenced by: [alert\$](#alert).
|
||||
|
||||
### Observer
|
||||
|
||||
This file contains non-react related core types used throught Backstage.
|
||||
|
||||
Observer interface for consuming an Observer, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Observer<T> = {
|
||||
next?(value: T): void;
|
||||
error?(error: Error): void;
|
||||
complete?(): void;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
|
||||
### Subscription
|
||||
|
||||
Subscription returned when subscribing to an Observable, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Subscription = {
|
||||
/**
|
||||
* Cancels the subscription
|
||||
*/
|
||||
unsubscribe(): void;
|
||||
|
||||
/**
|
||||
* Value indicating whether the subscription is closed.
|
||||
*/
|
||||
readonly closed: Boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
@@ -0,0 +1,225 @@
|
||||
# AppThemeApi
|
||||
|
||||
The AppThemeApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/AppThemeApi.ts:50](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L50).
|
||||
|
||||
The following Utility API implements this type:
|
||||
[appThemeApiRef](./README.md#apptheme)
|
||||
|
||||
## Members
|
||||
|
||||
### getInstalledThemes()
|
||||
|
||||
Get a list of available themes.
|
||||
|
||||
<pre>
|
||||
getInstalledThemes(): <a href="#apptheme">AppTheme</a>[]
|
||||
</pre>
|
||||
|
||||
### activeThemeId\$()
|
||||
|
||||
Observe the currently selected theme. A value of undefined means no specific
|
||||
theme has been selected.
|
||||
|
||||
<pre>
|
||||
activeThemeId$(): <a href="#observable">Observable</a><string | undefined>
|
||||
</pre>
|
||||
|
||||
### getActiveThemeId()
|
||||
|
||||
Get the current theme ID. Returns undefined if no specific theme is selected.
|
||||
|
||||
<pre>
|
||||
getActiveThemeId(): string | undefined
|
||||
</pre>
|
||||
|
||||
### setActiveThemeId()
|
||||
|
||||
Set a specific theme to use in the app, overriding the default theme selection.
|
||||
|
||||
Clear the selection by passing in undefined.
|
||||
|
||||
<pre>
|
||||
setActiveThemeId(themeId?: string): void
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### AppTheme
|
||||
|
||||
Describes a theme provided by the app.
|
||||
|
||||
<pre>
|
||||
export type AppTheme = {
|
||||
/**
|
||||
* ID used to remember theme selections.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Title of the theme
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Theme variant
|
||||
*/
|
||||
variant: 'light' | 'dark';
|
||||
|
||||
/**
|
||||
* The specialized MaterialUI theme instance.
|
||||
*/
|
||||
theme: <a href="#backstagetheme">BackstageTheme</a>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/AppThemeApi.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L24).
|
||||
|
||||
Referenced by: [getInstalledThemes](#getinstalledthemes).
|
||||
|
||||
### BackstagePalette
|
||||
|
||||
<pre>
|
||||
export type BackstagePalette = Palette & <a href="#paletteadditions">PaletteAdditions</a>
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/theme/src/types.ts:63](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L63).
|
||||
|
||||
Referenced by: [BackstageTheme](#backstagetheme).
|
||||
|
||||
### BackstageTheme
|
||||
|
||||
<pre>
|
||||
export interface BackstageTheme extends Theme {
|
||||
palette: <a href="#backstagepalette">BackstagePalette</a>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/theme/src/types.ts:66](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L66).
|
||||
|
||||
Referenced by: [AppTheme](#apptheme).
|
||||
|
||||
### Observable
|
||||
|
||||
Observable sequence of values and errors, see TC39.
|
||||
|
||||
https://github.com/tc39/proposal-observable
|
||||
|
||||
This is used as a common return type for observable values and can be created
|
||||
using many different observable implementations, such as zen-observable or
|
||||
RxJS 5.
|
||||
|
||||
<pre>
|
||||
export type Observable<T> = {
|
||||
/**
|
||||
* Subscribes to this observable to start receiving new values.
|
||||
*/
|
||||
subscribe(observer: <a href="#observer">Observer</a><T>): <a href="#subscription">Subscription</a>;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: Error) => void,
|
||||
onComplete?: () => void,
|
||||
): <a href="#subscription">Subscription</a>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
|
||||
|
||||
Referenced by: [activeThemeId\$](#activethemeid).
|
||||
|
||||
### Observer
|
||||
|
||||
This file contains non-react related core types used throught Backstage.
|
||||
|
||||
Observer interface for consuming an Observer, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Observer<T> = {
|
||||
next?(value: T): void;
|
||||
error?(error: Error): void;
|
||||
complete?(): void;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
|
||||
### PaletteAdditions
|
||||
|
||||
<pre>
|
||||
type PaletteAdditions = {
|
||||
status: {
|
||||
ok: string;
|
||||
warning: string;
|
||||
error: string;
|
||||
pending: string;
|
||||
running: string;
|
||||
aborted: string;
|
||||
};
|
||||
border: string;
|
||||
textContrast: string;
|
||||
textVerySubtle: string;
|
||||
textSubtle: string;
|
||||
highlight: string;
|
||||
errorBackground: string;
|
||||
warningBackground: string;
|
||||
infoBackground: string;
|
||||
errorText: string;
|
||||
infoText: string;
|
||||
warningText: string;
|
||||
linkHover: string;
|
||||
link: string;
|
||||
gold: string;
|
||||
sidebar: string;
|
||||
tabbar: {
|
||||
indicator: string;
|
||||
};
|
||||
bursts: {
|
||||
fontColor: string;
|
||||
slackChannelText: string;
|
||||
backgroundColor: {
|
||||
default: string;
|
||||
};
|
||||
};
|
||||
pinSidebarButton: {
|
||||
icon: string;
|
||||
background: string;
|
||||
};
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/theme/src/types.ts:23](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/theme/src/types.ts#L23).
|
||||
|
||||
Referenced by: [BackstagePalette](#backstagepalette).
|
||||
|
||||
### Subscription
|
||||
|
||||
Subscription returned when subscribing to an Observable, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Subscription = {
|
||||
/**
|
||||
* Cancels the subscription
|
||||
*/
|
||||
unsubscribe(): void;
|
||||
|
||||
/**
|
||||
* Value indicating whether the subscription is closed.
|
||||
*/
|
||||
readonly closed: Boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
@@ -0,0 +1,88 @@
|
||||
# BackstageIdentityApi
|
||||
|
||||
The BackstageIdentityApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:144](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L144).
|
||||
|
||||
The following Utility APIs implement this type:
|
||||
|
||||
- [githubAuthApiRef](./README.md#githubauth)
|
||||
|
||||
- [gitlabAuthApiRef](./README.md#gitlabauth)
|
||||
|
||||
- [googleAuthApiRef](./README.md#googleauth)
|
||||
|
||||
- [oktaAuthApiRef](./README.md#oktaauth)
|
||||
|
||||
## Members
|
||||
|
||||
### getBackstageIdentity()
|
||||
|
||||
Get the user's identity within Backstage. This should normally not be called
|
||||
directly, use the @IdentityApi instead.
|
||||
|
||||
If the optional flag is not set, a session is guaranteed to be returned, while
|
||||
if the optional flag is set, the session may be undefined. See
|
||||
@AuthRequestOptions for more details.
|
||||
|
||||
<pre>
|
||||
getBackstageIdentity(
|
||||
options?: <a href="#authrequestoptions">AuthRequestOptions</a>,
|
||||
): Promise<<a href="#backstageidentity">BackstageIdentity</a> | undefined>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### AuthRequestOptions
|
||||
|
||||
<pre>
|
||||
export type AuthRequestOptions = {
|
||||
/**
|
||||
* If this is set to true, the user will not be prompted to log in,
|
||||
* and an empty response will be returned if there is no existing session.
|
||||
*
|
||||
* This can be used to perform a check whether the user is logged in, or if you don't
|
||||
* want to force a user to be logged in, but provide functionality if they already are.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
|
||||
/**
|
||||
* If this is set to true, the request will bypass the regular oauth login modal
|
||||
* and open the login popup directly.
|
||||
*
|
||||
* The method must be called synchronously from a user action for this to work in all browsers.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
instantPopup?: boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
|
||||
|
||||
Referenced by: [getBackstageIdentity](#getbackstageidentity).
|
||||
|
||||
### BackstageIdentity
|
||||
|
||||
<pre>
|
||||
export type BackstageIdentity = {
|
||||
/**
|
||||
* The backstage user ID.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* An ID token that can be used to authenticate the user within Backstage.
|
||||
*/
|
||||
idToken: string;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:157](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L157).
|
||||
|
||||
Referenced by: [getBackstageIdentity](#getbackstageidentity).
|
||||
@@ -0,0 +1,179 @@
|
||||
# Config
|
||||
|
||||
The Config type is defined at
|
||||
[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L32).
|
||||
|
||||
The following Utility API implements this type:
|
||||
[configApiRef](./README.md#config)
|
||||
|
||||
## Members
|
||||
|
||||
### keys()
|
||||
|
||||
<pre>
|
||||
keys(): string[]
|
||||
</pre>
|
||||
|
||||
### get()
|
||||
|
||||
<pre>
|
||||
get(key: string): <a href="#jsonvalue">JsonValue</a>
|
||||
</pre>
|
||||
|
||||
### getOptional()
|
||||
|
||||
<pre>
|
||||
getOptional(key: string): <a href="#jsonvalue">JsonValue</a> | undefined
|
||||
</pre>
|
||||
|
||||
### getConfig()
|
||||
|
||||
<pre>
|
||||
getConfig(key: string): <a href="#config">Config</a>
|
||||
</pre>
|
||||
|
||||
### getOptionalConfig()
|
||||
|
||||
<pre>
|
||||
getOptionalConfig(key: string): <a href="#config">Config</a> | undefined
|
||||
</pre>
|
||||
|
||||
### getConfigArray()
|
||||
|
||||
<pre>
|
||||
getConfigArray(key: string): <a href="#config">Config</a>[]
|
||||
</pre>
|
||||
|
||||
### getOptionalConfigArray()
|
||||
|
||||
<pre>
|
||||
getOptionalConfigArray(key: string): <a href="#config">Config</a>[] | undefined
|
||||
</pre>
|
||||
|
||||
### getNumber()
|
||||
|
||||
<pre>
|
||||
getNumber(key: string): number
|
||||
</pre>
|
||||
|
||||
### getOptionalNumber()
|
||||
|
||||
<pre>
|
||||
getOptionalNumber(key: string): number | undefined
|
||||
</pre>
|
||||
|
||||
### getBoolean()
|
||||
|
||||
<pre>
|
||||
getBoolean(key: string): boolean
|
||||
</pre>
|
||||
|
||||
### getOptionalBoolean()
|
||||
|
||||
<pre>
|
||||
getOptionalBoolean(key: string): boolean | undefined
|
||||
</pre>
|
||||
|
||||
### getString()
|
||||
|
||||
<pre>
|
||||
getString(key: string): string
|
||||
</pre>
|
||||
|
||||
### getOptionalString()
|
||||
|
||||
<pre>
|
||||
getOptionalString(key: string): string | undefined
|
||||
</pre>
|
||||
|
||||
### getStringArray()
|
||||
|
||||
<pre>
|
||||
getStringArray(key: string): string[]
|
||||
</pre>
|
||||
|
||||
### getOptionalStringArray()
|
||||
|
||||
<pre>
|
||||
getOptionalStringArray(key: string): string[] | undefined
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### Config
|
||||
|
||||
<pre>
|
||||
export type Config = {
|
||||
keys(): string[];
|
||||
|
||||
get(key: string): <a href="#jsonvalue">JsonValue</a>;
|
||||
getOptional(key: string): <a href="#jsonvalue">JsonValue</a> | undefined;
|
||||
|
||||
getConfig(key: string): Config;
|
||||
getOptionalConfig(key: string): <a href="#config">Config</a> | undefined;
|
||||
|
||||
getConfigArray(key: string): <a href="#config">Config</a>[];
|
||||
getOptionalConfigArray(key: string): <a href="#config">Config</a>[] | undefined;
|
||||
|
||||
getNumber(key: string): number;
|
||||
getOptionalNumber(key: string): number | undefined;
|
||||
|
||||
getBoolean(key: string): boolean;
|
||||
getOptionalBoolean(key: string): boolean | undefined;
|
||||
|
||||
getString(key: string): string;
|
||||
getOptionalString(key: string): string | undefined;
|
||||
|
||||
getStringArray(key: string): string[];
|
||||
getOptionalStringArray(key: string): string[] | undefined;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/config/src/types.ts:32](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L32).
|
||||
|
||||
Referenced by: [getConfig](#getconfig), [getOptionalConfig](#getoptionalconfig),
|
||||
[getConfigArray](#getconfigarray),
|
||||
[getOptionalConfigArray](#getoptionalconfigarray), [Config](#config).
|
||||
|
||||
### JsonArray
|
||||
|
||||
<pre>
|
||||
export type JsonArray = <a href="#jsonvalue">JsonValue</a>[]
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/config/src/types.ts:18](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L18).
|
||||
|
||||
Referenced by: [JsonValue](#jsonvalue).
|
||||
|
||||
### JsonObject
|
||||
|
||||
<pre>
|
||||
export type JsonObject = { [key in string]?: <a href="#jsonvalue">JsonValue</a> }
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/config/src/types.ts:17](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L17).
|
||||
|
||||
Referenced by: [JsonValue](#jsonvalue).
|
||||
|
||||
### JsonValue
|
||||
|
||||
<pre>
|
||||
export type JsonValue =
|
||||
| <a href="#jsonobject">JsonObject</a>
|
||||
| <a href="#jsonarray">JsonArray</a>
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| null
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/config/src/types.ts:19](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/config/src/types.ts#L19).
|
||||
|
||||
Referenced by: [get](#get), [getOptional](#getoptional),
|
||||
[JsonObject](#jsonobject), [JsonArray](#jsonarray), [Config](#config).
|
||||
@@ -0,0 +1,134 @@
|
||||
# ErrorApi
|
||||
|
||||
The ErrorApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L53).
|
||||
|
||||
The following Utility API implements this type: [errorApiRef](./README.md#error)
|
||||
|
||||
## Members
|
||||
|
||||
### post()
|
||||
|
||||
Post an error for handling by the application.
|
||||
|
||||
<pre>
|
||||
post(error: <a href="#error">Error</a>, context?: <a href="#errorcontext">ErrorContext</a>): void
|
||||
</pre>
|
||||
|
||||
### error\$()
|
||||
|
||||
Observe errors posted by other parts of the application.
|
||||
|
||||
<pre>
|
||||
error$(): <a href="#observable">Observable</a><{ error: <a href="#error">Error</a>; context?: <a href="#errorcontext">ErrorContext</a> }>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### Error
|
||||
|
||||
Mirrors the javascript Error class, for the purpose of providing documentation
|
||||
and optional fields.
|
||||
|
||||
<pre>
|
||||
type Error = {
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L24).
|
||||
|
||||
Referenced by: [post](#post), [error\$](#error).
|
||||
|
||||
### ErrorContext
|
||||
|
||||
Provides additional information about an error that was posted to the
|
||||
application.
|
||||
|
||||
<pre>
|
||||
export type ErrorContext = {
|
||||
// If set to true, this error should not be displayed to the user. Defaults to false.
|
||||
hidden?: boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L33).
|
||||
|
||||
Referenced by: [post](#post), [error\$](#error).
|
||||
|
||||
### Observable
|
||||
|
||||
Observable sequence of values and errors, see TC39.
|
||||
|
||||
https://github.com/tc39/proposal-observable
|
||||
|
||||
This is used as a common return type for observable values and can be created
|
||||
using many different observable implementations, such as zen-observable or
|
||||
RxJS 5.
|
||||
|
||||
<pre>
|
||||
export type Observable<T> = {
|
||||
/**
|
||||
* Subscribes to this observable to start receiving new values.
|
||||
*/
|
||||
subscribe(observer: <a href="#observer">Observer</a><T>): <a href="#subscription">Subscription</a>;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: Error) => void,
|
||||
onComplete?: () => void,
|
||||
): <a href="#subscription">Subscription</a>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
|
||||
|
||||
Referenced by: [error\$](#error).
|
||||
|
||||
### Observer
|
||||
|
||||
This file contains non-react related core types used throught Backstage.
|
||||
|
||||
Observer interface for consuming an Observer, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Observer<T> = {
|
||||
next?(value: T): void;
|
||||
error?(error: Error): void;
|
||||
complete?(): void;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
|
||||
### Subscription
|
||||
|
||||
Subscription returned when subscribing to an Observable, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Subscription = {
|
||||
/**
|
||||
* Cancels the subscription
|
||||
*/
|
||||
unsubscribe(): void;
|
||||
|
||||
/**
|
||||
* Value indicating whether the subscription is closed.
|
||||
*/
|
||||
readonly closed: Boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
@@ -0,0 +1,33 @@
|
||||
# FeatureFlagsApi
|
||||
|
||||
The FeatureFlagsApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:41](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L41).
|
||||
|
||||
The following Utility API implements this type:
|
||||
[featureFlagsApiRef](./README.md#featureflags)
|
||||
|
||||
## Members
|
||||
|
||||
### registeredFeatureFlags
|
||||
|
||||
Store a list of registered feature flags.
|
||||
|
||||
<pre>
|
||||
registeredFeatureFlags: FeatureFlagsRegistryItem[]
|
||||
</pre>
|
||||
|
||||
### getFlags()
|
||||
|
||||
Get a list of all feature flags from the current user.
|
||||
|
||||
<pre>
|
||||
getFlags(): UserFlags
|
||||
</pre>
|
||||
|
||||
### getRegisteredFlags()
|
||||
|
||||
Get a list of all registered flags.
|
||||
|
||||
<pre>
|
||||
getRegisteredFlags(): FeatureFlagsRegistry
|
||||
</pre>
|
||||
@@ -0,0 +1,81 @@
|
||||
# IdentityApi
|
||||
|
||||
The IdentityApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/IdentityApi.ts#L22).
|
||||
|
||||
The following Utility API implements this type:
|
||||
[identityApiRef](./README.md#identity)
|
||||
|
||||
## Members
|
||||
|
||||
### getUserId()
|
||||
|
||||
The ID of the signed in user. This ID is not meant to be presented to the user,
|
||||
but used as an opaque string to pass on to backends or use in frontend logic.
|
||||
|
||||
TODO: The intention of the user ID is to be able to tie the user to an identity
|
||||
that is known by the catalog and/or identity backend. It should for example be
|
||||
possible to fetch all owned components using this ID.
|
||||
|
||||
<pre>
|
||||
getUserId(): string
|
||||
</pre>
|
||||
|
||||
### getProfile()
|
||||
|
||||
The profile of the signed in user.
|
||||
|
||||
<pre>
|
||||
getProfile(): <a href="#profileinfo">ProfileInfo</a>
|
||||
</pre>
|
||||
|
||||
### getIdToken()
|
||||
|
||||
An OpenID Connect ID Token which proves the identity of the signed in user.
|
||||
|
||||
The ID token will be undefined if the signed in user does not have a verified
|
||||
identity, such as a demo user or mocked user for e2e tests.
|
||||
|
||||
<pre>
|
||||
getIdToken(): Promise<string | undefined>
|
||||
</pre>
|
||||
|
||||
### logout()
|
||||
|
||||
Log out the current user
|
||||
|
||||
<pre>
|
||||
logout(): Promise<void>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### ProfileInfo
|
||||
|
||||
Profile information of the user.
|
||||
|
||||
<pre>
|
||||
export type ProfileInfo = {
|
||||
/**
|
||||
* Email ID.
|
||||
*/
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* Display name that can be presented to the user.
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* URL to an avatar image of the user.
|
||||
*/
|
||||
picture?: string;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L172).
|
||||
|
||||
Referenced by: [getProfile](#getprofile).
|
||||
@@ -0,0 +1,119 @@
|
||||
# OAuthApi
|
||||
|
||||
The OAuthApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L67).
|
||||
|
||||
The following Utility APIs implement this type:
|
||||
|
||||
- [githubAuthApiRef](./README.md#githubauth)
|
||||
|
||||
- [gitlabAuthApiRef](./README.md#gitlabauth)
|
||||
|
||||
- [googleAuthApiRef](./README.md#googleauth)
|
||||
|
||||
- [oauth2ApiRef](./README.md#oauth2)
|
||||
|
||||
- [oktaAuthApiRef](./README.md#oktaauth)
|
||||
|
||||
## Members
|
||||
|
||||
### getAccessToken()
|
||||
|
||||
Requests an OAuth 2 Access Token, optionally with a set of scopes. The access
|
||||
token allows you to make requests on behalf of the user, and the copes may grant
|
||||
you broader access, depending on the auth provider.
|
||||
|
||||
Each auth provider has separate handling of scope, so you need to look at the
|
||||
documentation for each one to know what scope you need to request.
|
||||
|
||||
This method is cheap and should be called each time an access token is used. Do
|
||||
not for example store the access token in React component state, as that could
|
||||
cause the token to expire. Instead fetch a new access token for each request.
|
||||
|
||||
Be sure to include all required scopes when requesting an access token. When
|
||||
testing your implementation it is best to log out the Backstage session and then
|
||||
visit your plugin page directly, as you might already have some required scopes
|
||||
in your existing session. Not requesting the correct scopes can lead to 403 or
|
||||
other authorization errors, which can be tricky to debug.
|
||||
|
||||
If the user has not yet granted access to the provider and the set of requested
|
||||
scopes, the user will be prompted to log in. The returned promise will not
|
||||
resolve until the user has successfully logged in. The returned promise can be
|
||||
rejected, but only if the user rejects the login request.
|
||||
|
||||
<pre>
|
||||
getAccessToken(
|
||||
scope?: <a href="#oauthscope">OAuthScope</a>,
|
||||
options?: <a href="#authrequestoptions">AuthRequestOptions</a>,
|
||||
): Promise<string>
|
||||
</pre>
|
||||
|
||||
### logout()
|
||||
|
||||
Log out the user's session. This will reload the page.
|
||||
|
||||
<pre>
|
||||
logout(): Promise<void>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### AuthRequestOptions
|
||||
|
||||
<pre>
|
||||
export type AuthRequestOptions = {
|
||||
/**
|
||||
* If this is set to true, the user will not be prompted to log in,
|
||||
* and an empty response will be returned if there is no existing session.
|
||||
*
|
||||
* This can be used to perform a check whether the user is logged in, or if you don't
|
||||
* want to force a user to be logged in, but provide functionality if they already are.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
|
||||
/**
|
||||
* If this is set to true, the request will bypass the regular oauth login modal
|
||||
* and open the login popup directly.
|
||||
*
|
||||
* The method must be called synchronously from a user action for this to work in all browsers.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
instantPopup?: boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
|
||||
|
||||
Referenced by: [getAccessToken](#getaccesstoken).
|
||||
|
||||
### OAuthScope
|
||||
|
||||
This file contains declarations for common interfaces of auth-related APIs. The
|
||||
declarations should be used to signal which type of authentication and
|
||||
authorization methods each separate auth provider supports.
|
||||
|
||||
For example, a Google OAuth provider that supports OAuth 2 and OpenID Connect,
|
||||
would be declared as follows:
|
||||
|
||||
const googleAuthApiRef = createApiRef<OAuthApi & OpenIDConnectApi>({ ... })
|
||||
|
||||
An array of scopes, or a scope string formatted according to the auth provider,
|
||||
which is typically a space separated list.
|
||||
|
||||
See the documentation for each auth provider for the list of scopes supported by
|
||||
each provider.
|
||||
|
||||
<pre>
|
||||
export type OAuthScope = string | string[]
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L38).
|
||||
|
||||
Referenced by: [getAccessToken](#getaccesstoken).
|
||||
@@ -0,0 +1,232 @@
|
||||
# OAuthRequestApi
|
||||
|
||||
The OAuthRequestApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99).
|
||||
|
||||
The following Utility API implements this type:
|
||||
[oauthRequestApiRef](./README.md#oauthrequest)
|
||||
|
||||
## Members
|
||||
|
||||
### createAuthRequester()
|
||||
|
||||
A utility for showing login popups or similar things, and merging together
|
||||
multiple requests for different scopes into one request that inclues all scopes.
|
||||
|
||||
The passed in options provide information about the login provider, and how to
|
||||
handle auth requests.
|
||||
|
||||
The returned AuthRequester function is used to request login with new scopes.
|
||||
These requests are merged together and forwarded to the auth handler, as soon as
|
||||
a consumer of auth requests triggers an auth flow.
|
||||
|
||||
See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info.
|
||||
|
||||
<pre>
|
||||
createAuthRequester<AuthResponse>(
|
||||
options: <a href="#authrequesteroptions">AuthRequesterOptions</a><AuthResponse>,
|
||||
): <a href="#authrequester">AuthRequester</a><AuthResponse>
|
||||
</pre>
|
||||
|
||||
### authRequest\$()
|
||||
|
||||
Observers panding auth requests. The returned observable will emit all current
|
||||
active auth request, at most one for each created auth requester.
|
||||
|
||||
Each request has its own info about the login provider, forwarded from the auth
|
||||
requester options.
|
||||
|
||||
Depending on user interaction, the request should either be rejected, or used to
|
||||
trigger the auth handler. If the request is rejected, all pending AuthRequester
|
||||
calls will fail with a "RejectedError". If a auth is triggered, and the auth
|
||||
handler resolves successfully, then all currently pending AuthRequester calls
|
||||
will resolve to the value returned by the onAuthRequest call.
|
||||
|
||||
<pre>
|
||||
authRequest$(): <a href="#observable">Observable</a><<a href="#pendingauthrequest">PendingAuthRequest</a>[]>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### AuthProvider
|
||||
|
||||
Information about the auth provider that we're requesting a login towards.
|
||||
|
||||
This should be shown to the user so that they can be informed about what login
|
||||
is being requested before a popup is shown.
|
||||
|
||||
<pre>
|
||||
export type AuthProvider = {
|
||||
/**
|
||||
* Title for the auth provider, for example "GitHub"
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Icon for the auth provider.
|
||||
*/
|
||||
icon: IconComponent;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27).
|
||||
|
||||
Referenced by: [AuthRequesterOptions](#authrequesteroptions),
|
||||
[PendingAuthRequest](#pendingauthrequest).
|
||||
|
||||
### AuthRequester
|
||||
|
||||
Function used to trigger new auth requests for a set of scopes.
|
||||
|
||||
The returned promise will resolve to the same value returned by the
|
||||
onAuthRequest in the AuthRequesterOptions. Or rejected, if the request is
|
||||
rejected.
|
||||
|
||||
This function can be called multiple times before the promise resolves. All
|
||||
calls will be merged into one request, and the scopes forwarded to the
|
||||
onAuthRequest will be the union of all requested scopes.
|
||||
|
||||
<pre>
|
||||
export type AuthRequester<AuthResponse> = (
|
||||
scopes: Set<string>,
|
||||
) => Promise<AuthResponse>
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66).
|
||||
|
||||
Referenced by: [createAuthRequester](#createauthrequester).
|
||||
|
||||
### AuthRequesterOptions
|
||||
|
||||
Describes how to handle auth requests. Both how to show them to the user, and
|
||||
what to do when the user accesses the auth request.
|
||||
|
||||
<pre>
|
||||
export type AuthRequesterOptions<AuthResponse> = {
|
||||
/**
|
||||
* Information about the auth provider, which will be forwarded to auth requests.
|
||||
*/
|
||||
provider: <a href="#authprovider">AuthProvider</a>;
|
||||
|
||||
/**
|
||||
* Implementation of the auth flow, which will be called synchronously when
|
||||
* trigger() is called on an auth requests.
|
||||
*/
|
||||
onAuthRequest(scopes: Set<string>): Promise<AuthResponse>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43).
|
||||
|
||||
Referenced by: [createAuthRequester](#createauthrequester).
|
||||
|
||||
### Observable
|
||||
|
||||
Observable sequence of values and errors, see TC39.
|
||||
|
||||
https://github.com/tc39/proposal-observable
|
||||
|
||||
This is used as a common return type for observable values and can be created
|
||||
using many different observable implementations, such as zen-observable or
|
||||
RxJS 5.
|
||||
|
||||
<pre>
|
||||
export type Observable<T> = {
|
||||
/**
|
||||
* Subscribes to this observable to start receiving new values.
|
||||
*/
|
||||
subscribe(observer: <a href="#observer">Observer</a><T>): <a href="#subscription">Subscription</a>;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: Error) => void,
|
||||
onComplete?: () => void,
|
||||
): <a href="#subscription">Subscription</a>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
|
||||
|
||||
Referenced by: [authRequest\$](#authrequest).
|
||||
|
||||
### Observer
|
||||
|
||||
This file contains non-react related core types used throught Backstage.
|
||||
|
||||
Observer interface for consuming an Observer, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Observer<T> = {
|
||||
next?(value: T): void;
|
||||
error?(error: Error): void;
|
||||
complete?(): void;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
|
||||
### PendingAuthRequest
|
||||
|
||||
An pending auth request for a single auth provider. The request will remain in
|
||||
this pending state until either reject() or trigger() is called.
|
||||
|
||||
Any new requests for the same provider are merged into the existing pending
|
||||
request, meaning there will only ever be a single pending request for a given
|
||||
provider.
|
||||
|
||||
<pre>
|
||||
export type PendingAuthRequest = {
|
||||
/**
|
||||
* Information about the auth provider, as given in the AuthRequesterOptions
|
||||
*/
|
||||
provider: <a href="#authprovider">AuthProvider</a>;
|
||||
|
||||
/**
|
||||
* Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError".
|
||||
*/
|
||||
reject: () => void;
|
||||
|
||||
/**
|
||||
* Trigger the auth request to continue the auth flow, by for example showing a popup.
|
||||
*
|
||||
* Synchronously calls onAuthRequest with all scope currently in the request.
|
||||
*/
|
||||
trigger(): Promise<void>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77).
|
||||
|
||||
Referenced by: [authRequest\$](#authrequest).
|
||||
|
||||
### Subscription
|
||||
|
||||
Subscription returned when subscribing to an Observable, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Subscription = {
|
||||
/**
|
||||
* Cancels the subscription
|
||||
*/
|
||||
unsubscribe(): void;
|
||||
|
||||
/**
|
||||
* Value indicating whether the subscription is closed.
|
||||
*/
|
||||
readonly closed: Boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
@@ -0,0 +1,75 @@
|
||||
# OpenIdConnectApi
|
||||
|
||||
The OpenIdConnectApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:104](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L104).
|
||||
|
||||
The following Utility APIs implement this type:
|
||||
|
||||
- [googleAuthApiRef](./README.md#googleauth)
|
||||
|
||||
- [oauth2ApiRef](./README.md#oauth2)
|
||||
|
||||
- [oktaAuthApiRef](./README.md#oktaauth)
|
||||
|
||||
## Members
|
||||
|
||||
### getIdToken()
|
||||
|
||||
Requests an OpenID Connect ID Token.
|
||||
|
||||
This method is cheap and should be called each time an ID token is used. Do not
|
||||
for example store the id token in React component state, as that could cause the
|
||||
token to expire. Instead fetch a new id token for each request.
|
||||
|
||||
If the user has not yet logged in to Google inside Backstage, the user will be
|
||||
prompted to log in. The returned promise will not resolve until the user has
|
||||
successfully logged in. The returned promise can be rejected, but only if the
|
||||
user rejects the login request.
|
||||
|
||||
<pre>
|
||||
getIdToken(options?: <a href="#authrequestoptions">AuthRequestOptions</a>): Promise<string>
|
||||
</pre>
|
||||
|
||||
### logout()
|
||||
|
||||
Log out the user's session. This will reload the page.
|
||||
|
||||
<pre>
|
||||
logout(): Promise<void>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### AuthRequestOptions
|
||||
|
||||
<pre>
|
||||
export type AuthRequestOptions = {
|
||||
/**
|
||||
* If this is set to true, the user will not be prompted to log in,
|
||||
* and an empty response will be returned if there is no existing session.
|
||||
*
|
||||
* This can be used to perform a check whether the user is logged in, or if you don't
|
||||
* want to force a user to be logged in, but provide functionality if they already are.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
|
||||
/**
|
||||
* If this is set to true, the request will bypass the regular oauth login modal
|
||||
* and open the login popup directly.
|
||||
*
|
||||
* The method must be called synchronously from a user action for this to work in all browsers.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
instantPopup?: boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
|
||||
|
||||
Referenced by: [getIdToken](#getidtoken).
|
||||
@@ -0,0 +1,94 @@
|
||||
# ProfileInfoApi
|
||||
|
||||
The ProfileInfoApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:127](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L127).
|
||||
|
||||
The following Utility APIs implement this type:
|
||||
|
||||
- [githubAuthApiRef](./README.md#githubauth)
|
||||
|
||||
- [gitlabAuthApiRef](./README.md#gitlabauth)
|
||||
|
||||
- [googleAuthApiRef](./README.md#googleauth)
|
||||
|
||||
- [oauth2ApiRef](./README.md#oauth2)
|
||||
|
||||
- [oktaAuthApiRef](./README.md#oktaauth)
|
||||
|
||||
## Members
|
||||
|
||||
### getProfile()
|
||||
|
||||
Get profile information for the user as supplied by this auth provider.
|
||||
|
||||
If the optional flag is not set, a session is guaranteed to be returned, while
|
||||
if the optional flag is set, the session may be undefined. See
|
||||
@AuthRequestOptions for more details.
|
||||
|
||||
<pre>
|
||||
getProfile(options?: <a href="#authrequestoptions">AuthRequestOptions</a>): Promise<<a href="#profileinfo">ProfileInfo</a> | undefined>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### AuthRequestOptions
|
||||
|
||||
<pre>
|
||||
export type AuthRequestOptions = {
|
||||
/**
|
||||
* If this is set to true, the user will not be prompted to log in,
|
||||
* and an empty response will be returned if there is no existing session.
|
||||
*
|
||||
* This can be used to perform a check whether the user is logged in, or if you don't
|
||||
* want to force a user to be logged in, but provide functionality if they already are.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
|
||||
/**
|
||||
* If this is set to true, the request will bypass the regular oauth login modal
|
||||
* and open the login popup directly.
|
||||
*
|
||||
* The method must be called synchronously from a user action for this to work in all browsers.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
instantPopup?: boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L40).
|
||||
|
||||
Referenced by: [getProfile](#getprofile).
|
||||
|
||||
### ProfileInfo
|
||||
|
||||
Profile information of the user.
|
||||
|
||||
<pre>
|
||||
export type ProfileInfo = {
|
||||
/**
|
||||
* Email ID.
|
||||
*/
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* Display name that can be presented to the user.
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* URL to an avatar image of the user.
|
||||
*/
|
||||
picture?: string;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:172](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L172).
|
||||
|
||||
Referenced by: [getProfile](#getprofile).
|
||||
@@ -0,0 +1,139 @@
|
||||
# Backstage Core Utility APIs
|
||||
|
||||
The following is a list of all Utility APIs defined by `@backstage/core`. They
|
||||
are available to use by plugins and components, and can be accessed using the
|
||||
`useApi` hook, also provided by `@backstage/core`. For more information, see
|
||||
https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md.
|
||||
|
||||
### alert
|
||||
|
||||
Used to report alerts and forward them to the app
|
||||
|
||||
Implemented type: [AlertApi](./AlertApi.md)
|
||||
|
||||
ApiRef:
|
||||
[alertApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AlertApi.ts#L41)
|
||||
|
||||
### appTheme
|
||||
|
||||
API Used to configure the app theme, and enumerate options
|
||||
|
||||
Implemented type: [AppThemeApi](./AppThemeApi.md)
|
||||
|
||||
ApiRef:
|
||||
[appThemeApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/AppThemeApi.ts#L74)
|
||||
|
||||
### config
|
||||
|
||||
Used to access runtime configuration
|
||||
|
||||
Implemented type: [Config](./Config.md)
|
||||
|
||||
ApiRef:
|
||||
[configApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ConfigApi.ts#L22)
|
||||
|
||||
### error
|
||||
|
||||
Used to report errors and forward them to the app
|
||||
|
||||
Implemented type: [ErrorApi](./ErrorApi.md)
|
||||
|
||||
ApiRef:
|
||||
[errorApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/ErrorApi.ts#L65)
|
||||
|
||||
### featureFlags
|
||||
|
||||
Used to toggle functionality in features across Backstage
|
||||
|
||||
Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md)
|
||||
|
||||
ApiRef:
|
||||
[featureFlagsApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L58)
|
||||
|
||||
### githubAuth
|
||||
|
||||
Provides authentication towards Github APIs
|
||||
|
||||
Implemented types: [OAuthApi](./OAuthApi.md),
|
||||
[ProfileInfoApi](./ProfileInfoApi.md),
|
||||
[BackstageIdentityApi](./BackstageIdentityApi.md),
|
||||
[SessionStateApi](./SessionStateApi.md)
|
||||
|
||||
ApiRef:
|
||||
[githubAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L230)
|
||||
|
||||
### gitlabAuth
|
||||
|
||||
Provides authentication towards Gitlab APIs
|
||||
|
||||
Implemented types: [OAuthApi](./OAuthApi.md),
|
||||
[ProfileInfoApi](./ProfileInfoApi.md),
|
||||
[BackstageIdentityApi](./BackstageIdentityApi.md),
|
||||
[SessionStateApi](./SessionStateApi.md)
|
||||
|
||||
ApiRef:
|
||||
[gitlabAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L260)
|
||||
|
||||
### googleAuth
|
||||
|
||||
Provides authentication towards Google APIs and identities
|
||||
|
||||
Implemented types: [OAuthApi](./OAuthApi.md),
|
||||
[OpenIdConnectApi](./OpenIdConnectApi.md),
|
||||
[ProfileInfoApi](./ProfileInfoApi.md),
|
||||
[BackstageIdentityApi](./BackstageIdentityApi.md),
|
||||
[SessionStateApi](./SessionStateApi.md)
|
||||
|
||||
ApiRef:
|
||||
[googleAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L213)
|
||||
|
||||
### identity
|
||||
|
||||
Provides access to the identity of the signed in user
|
||||
|
||||
Implemented type: [IdentityApi](./IdentityApi.md)
|
||||
|
||||
ApiRef:
|
||||
[identityApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/IdentityApi.ts#L54)
|
||||
|
||||
### oauth2
|
||||
|
||||
Example of how to use oauth2 custom provider
|
||||
|
||||
Implemented types: [OAuthApi](./OAuthApi.md),
|
||||
[OpenIdConnectApi](./OpenIdConnectApi.md),
|
||||
[ProfileInfoApi](./ProfileInfoApi.md), [SessionStateApi](./SessionStateApi.md)
|
||||
|
||||
ApiRef:
|
||||
[oauth2ApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L270)
|
||||
|
||||
### oauthRequest
|
||||
|
||||
An API for implementing unified OAuth flows in Backstage
|
||||
|
||||
Implemented type: [OAuthRequestApi](./OAuthRequestApi.md)
|
||||
|
||||
ApiRef:
|
||||
[oauthRequestApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130)
|
||||
|
||||
### oktaAuth
|
||||
|
||||
Provides authentication towards Okta APIs
|
||||
|
||||
Implemented types: [OAuthApi](./OAuthApi.md),
|
||||
[OpenIdConnectApi](./OpenIdConnectApi.md),
|
||||
[ProfileInfoApi](./ProfileInfoApi.md),
|
||||
[BackstageIdentityApi](./BackstageIdentityApi.md),
|
||||
[SessionStateApi](./SessionStateApi.md)
|
||||
|
||||
ApiRef:
|
||||
[oktaAuthApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L243)
|
||||
|
||||
### storage
|
||||
|
||||
Provides the ability to store data which is unique to the user
|
||||
|
||||
Implemented type: [StorageApi](./StorageApi.md)
|
||||
|
||||
ApiRef:
|
||||
[storageApiRef](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L68)
|
||||
@@ -0,0 +1,115 @@
|
||||
# SessionStateApi
|
||||
|
||||
The SessionStateApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:201](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L201).
|
||||
|
||||
The following Utility APIs implement this type:
|
||||
|
||||
- [githubAuthApiRef](./README.md#githubauth)
|
||||
|
||||
- [gitlabAuthApiRef](./README.md#gitlabauth)
|
||||
|
||||
- [googleAuthApiRef](./README.md#googleauth)
|
||||
|
||||
- [oauth2ApiRef](./README.md#oauth2)
|
||||
|
||||
- [oktaAuthApiRef](./README.md#oktaauth)
|
||||
|
||||
## Members
|
||||
|
||||
### sessionState\$()
|
||||
|
||||
<pre>
|
||||
sessionState$(): <a href="#observable">Observable</a><<a href="#sessionstate">SessionState</a>>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### Observable
|
||||
|
||||
Observable sequence of values and errors, see TC39.
|
||||
|
||||
https://github.com/tc39/proposal-observable
|
||||
|
||||
This is used as a common return type for observable values and can be created
|
||||
using many different observable implementations, such as zen-observable or
|
||||
RxJS 5.
|
||||
|
||||
<pre>
|
||||
export type Observable<T> = {
|
||||
/**
|
||||
* Subscribes to this observable to start receiving new values.
|
||||
*/
|
||||
subscribe(observer: <a href="#observer">Observer</a><T>): <a href="#subscription">Subscription</a>;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: Error) => void,
|
||||
onComplete?: () => void,
|
||||
): <a href="#subscription">Subscription</a>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
|
||||
|
||||
Referenced by: [sessionState\$](#sessionstate).
|
||||
|
||||
### Observer
|
||||
|
||||
This file contains non-react related core types used throught Backstage.
|
||||
|
||||
Observer interface for consuming an Observer, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Observer<T> = {
|
||||
next?(value: T): void;
|
||||
error?(error: Error): void;
|
||||
complete?(): void;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
|
||||
### SessionState
|
||||
|
||||
Session state values passed to subscribers of the SessionStateApi.
|
||||
|
||||
<pre>
|
||||
export enum SessionState {
|
||||
SignedIn = 'SignedIn',
|
||||
SignedOut = 'SignedOut',
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/auth.ts:192](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/auth.ts#L192).
|
||||
|
||||
Referenced by: [sessionState\$](#sessionstate).
|
||||
|
||||
### Subscription
|
||||
|
||||
Subscription returned when subscribing to an Observable, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Subscription = {
|
||||
/**
|
||||
* Cancels the subscription
|
||||
*/
|
||||
unsubscribe(): void;
|
||||
|
||||
/**
|
||||
* Value indicating whether the subscription is closed.
|
||||
*/
|
||||
readonly closed: Boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
@@ -0,0 +1,186 @@
|
||||
# StorageApi
|
||||
|
||||
The StorageApi type is defined at
|
||||
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
|
||||
|
||||
The following Utility API implements this type:
|
||||
[storageApiRef](./README.md#storage)
|
||||
|
||||
## Members
|
||||
|
||||
### forBucket()
|
||||
|
||||
Create a bucket to store data in.
|
||||
|
||||
<pre>
|
||||
forBucket(name: string): <a href="#storageapi">StorageApi</a>
|
||||
</pre>
|
||||
|
||||
### get()
|
||||
|
||||
Get the current value for persistent data, use observe\$ to be notified of
|
||||
updates.
|
||||
|
||||
<pre>
|
||||
get<T>(key: string): T | undefined
|
||||
</pre>
|
||||
|
||||
### remove()
|
||||
|
||||
Remove persistent data.
|
||||
|
||||
<pre>
|
||||
remove(key: string): Promise<void>
|
||||
</pre>
|
||||
|
||||
### set()
|
||||
|
||||
Save persistant data, and emit messages to anyone that is using observe\$ for
|
||||
this key
|
||||
|
||||
<pre>
|
||||
set(key: string, data: any): Promise<void>
|
||||
</pre>
|
||||
|
||||
### observe\$()
|
||||
|
||||
Observe changes on a particular key in the bucket
|
||||
|
||||
<pre>
|
||||
observe$<T>(key: string): <a href="#observable">Observable</a><<a href="#storagevaluechange">StorageValueChange</a><T>>
|
||||
</pre>
|
||||
|
||||
## Supporting types
|
||||
|
||||
These types are part of the API declaration, but may not be unique to this API.
|
||||
|
||||
### Observable
|
||||
|
||||
Observable sequence of values and errors, see TC39.
|
||||
|
||||
https://github.com/tc39/proposal-observable
|
||||
|
||||
This is used as a common return type for observable values and can be created
|
||||
using many different observable implementations, such as zen-observable or
|
||||
RxJS 5.
|
||||
|
||||
<pre>
|
||||
export type Observable<T> = {
|
||||
/**
|
||||
* Subscribes to this observable to start receiving new values.
|
||||
*/
|
||||
subscribe(observer: <a href="#observer">Observer</a><T>): <a href="#subscription">Subscription</a>;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: Error) => void,
|
||||
onComplete?: () => void,
|
||||
): <a href="#subscription">Subscription</a>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:53](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L53).
|
||||
|
||||
Referenced by: [observe\$](#observe), [StorageApi](#storageapi).
|
||||
|
||||
### Observer
|
||||
|
||||
This file contains non-react related core types used throught Backstage.
|
||||
|
||||
Observer interface for consuming an Observer, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Observer<T> = {
|
||||
next?(value: T): void;
|
||||
error?(error: Error): void;
|
||||
complete?(): void;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:24](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L24).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
|
||||
### StorageApi
|
||||
|
||||
<pre>
|
||||
export interface StorageApi {
|
||||
/**
|
||||
* Create a bucket to store data in.
|
||||
* @param {String} name Namespace for the storage to be stored under,
|
||||
* will inherit previous namespaces too
|
||||
*/
|
||||
forBucket(name: string): StorageApi;
|
||||
|
||||
/**
|
||||
* Get the current value for persistent data, use observe$ to be notified of updates.
|
||||
*
|
||||
* @param {String} key Unique key associated with the data.
|
||||
* @return {Object} data The data that should is stored.
|
||||
*/
|
||||
get<T>(key: string): T | undefined;
|
||||
|
||||
/**
|
||||
* Remove persistent data.
|
||||
*
|
||||
* @param {String} key Unique key associated with the data.
|
||||
*/
|
||||
remove(key: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Save persistant data, and emit messages to anyone that is using observe$ for this key
|
||||
*
|
||||
* @param {String} key Unique key associated with the data.
|
||||
*/
|
||||
set(key: string, data: any): Promise<void>;
|
||||
|
||||
/**
|
||||
* Observe changes on a particular key in the bucket
|
||||
* @param {String} key Unique key associated with the data
|
||||
*/
|
||||
observe$<T>(key: string): <a href="#observable">Observable</a><<a href="#storagevaluechange">StorageValueChange</a><T>>;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
|
||||
|
||||
Referenced by: [forBucket](#forbucket).
|
||||
|
||||
### StorageValueChange
|
||||
|
||||
<pre>
|
||||
export type StorageValueChange<T = any> = {
|
||||
key: string;
|
||||
newValue?: T;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/apis/definitions/StorageApi.ts#L21).
|
||||
|
||||
Referenced by: [observe\$](#observe), [StorageApi](#storageapi).
|
||||
|
||||
### Subscription
|
||||
|
||||
Subscription returned when subscribing to an Observable, see TC39.
|
||||
|
||||
<pre>
|
||||
export type Subscription = {
|
||||
/**
|
||||
* Cancels the subscription
|
||||
*/
|
||||
unsubscribe(): void;
|
||||
|
||||
/**
|
||||
* Value indicating whether the subscription is closed.
|
||||
*/
|
||||
readonly closed: Boolean;
|
||||
}
|
||||
</pre>
|
||||
|
||||
Defined at
|
||||
[packages/core-api/src/types.ts:33](https://github.com/spotify/backstage/blob/53a229ea7576b1432835e54e41e0b9526038afa4/packages/core-api/src/types.ts#L33).
|
||||
|
||||
Referenced by: [Observable](#observable).
|
||||
@@ -91,6 +91,7 @@ nav:
|
||||
- ADR004 - Module Export Structure: 'architecture-decisions/adr004-module-export-structure.md'
|
||||
- ADR005 - Catalog Core Entities: 'architecture-decisions/adr005-catalog-core-entities.md'
|
||||
- ADR006 - Avoid React.FC and React.SFC: 'architecture-decisions/adr006-avoid-react-fc.md'
|
||||
- ADR007 - Use MSW for Network Request Mocking: 'architecture-decisions/adr007-use-msw-to-mock-service-requests.md'
|
||||
- Contribute: '../CONTRIBUTING.md'
|
||||
- Support: 'overview/support.md'
|
||||
- FAQ: FAQ.md
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"lint": "lerna run lint --since origin/master --",
|
||||
"lint:all": "lerna run lint --",
|
||||
"lint:type-deps": "node scripts/check-type-dependencies.js",
|
||||
"docgen": "lerna run docgen",
|
||||
"docker-build": "yarn workspace example-app build && docker build . -t spotify/backstage",
|
||||
"docker-build:all": "yarn tsc && yarn build && yarn docker-build && yarn workspace example-backend build-image",
|
||||
"create-plugin": "backstage-cli create-plugin",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/jquery": "^3.3.34",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
AlertDisplay,
|
||||
OAuthRequestDialog,
|
||||
SignInPage,
|
||||
useApi,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
import React, { FC } from 'react';
|
||||
import Root from './components/Root';
|
||||
@@ -30,12 +32,13 @@ const app = createApp({
|
||||
apis,
|
||||
plugins: Object.values(plugins),
|
||||
components: {
|
||||
SignInPage: props => (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={['guest', 'google', 'custom', 'okta', 'gitlab', 'github']}
|
||||
/>
|
||||
),
|
||||
SignInPage: props => {
|
||||
const configApi = useApi(configApiRef);
|
||||
const providersConfig = configApi.getOptionalConfig('auth.providers');
|
||||
const providers = providersConfig?.keys() ?? [];
|
||||
|
||||
return <SignInPage {...props} providers={['guest', ...providers]} />;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"helmet": "^3.22.0",
|
||||
"morgan": "^1.10.0",
|
||||
"stoppable": "^1.1.0",
|
||||
|
||||
@@ -28,9 +28,9 @@ export type ErrorHandlerOptions = {
|
||||
showStackTraces?: boolean;
|
||||
|
||||
/**
|
||||
* Logger
|
||||
* Logger instance to log any 5xx errors.
|
||||
*
|
||||
* If not specified, by default shows stack traces only in development mode.
|
||||
* If not specified, the root logger will be used.
|
||||
*/
|
||||
logger?: Logger;
|
||||
};
|
||||
|
||||
@@ -17,3 +17,4 @@
|
||||
export * from './errorHandler';
|
||||
export * from './notFoundHandler';
|
||||
export * from './requestLoggingHandler';
|
||||
export * from './statusCheckHandler';
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 express from 'express';
|
||||
import request from 'supertest';
|
||||
import { statusCheckHandler } from './statusCheckHandler';
|
||||
|
||||
describe('statusCheckHandler', () => {
|
||||
it('gives status 200 when using default', async () => {
|
||||
const app = express();
|
||||
app.use('/healthcheck', await statusCheckHandler());
|
||||
|
||||
const response = await request(app).get('/healthcheck');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
|
||||
});
|
||||
|
||||
it('gives status 200 when status function returns true', async () => {
|
||||
const app = express();
|
||||
const status = { foo: 'bar' };
|
||||
const statusCheck = () => Promise.resolve(status);
|
||||
app.use('/healthcheck', await statusCheckHandler({ statusCheck }));
|
||||
|
||||
const response = await request(app).get('/healthcheck');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toBe(JSON.stringify(status));
|
||||
});
|
||||
|
||||
it('gives status 500 when status check throws an error', async () => {
|
||||
const app = express();
|
||||
const statusCheck = () => {
|
||||
throw Error('error!');
|
||||
};
|
||||
app.use('/healthcheck', await statusCheckHandler({ statusCheck }));
|
||||
|
||||
const response = await request(app).get('/healthcheck');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { NextFunction, Request, Response, RequestHandler } from 'express';
|
||||
|
||||
export type StatusCheck = () => Promise<any>;
|
||||
|
||||
export interface StatusCheckHandlerOptions {
|
||||
/**
|
||||
* Optional status function which returns a message.
|
||||
*/
|
||||
statusCheck?: StatusCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Express middleware for status checks.
|
||||
*
|
||||
* This is commonly used to implement healthcheck and readiness routes.
|
||||
*
|
||||
* @param options An optional configuration object.
|
||||
* @returns An Express error request handler
|
||||
*/
|
||||
export async function statusCheckHandler(
|
||||
options: StatusCheckHandlerOptions = {},
|
||||
): Promise<RequestHandler> {
|
||||
const statusCheck: StatusCheck = options.statusCheck
|
||||
? options.statusCheck
|
||||
: () => Promise.resolve({ status: 'ok' });
|
||||
|
||||
return async (_request: Request, response: Response, next: NextFunction) => {
|
||||
try {
|
||||
const status = await statusCheck();
|
||||
response.status(200).header('').send(status);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 express from 'express';
|
||||
import * as winston from 'winston';
|
||||
import request from 'supertest';
|
||||
import { createStatusCheckRouter } from './createStatusCheckRouter';
|
||||
|
||||
describe('createStatusCheckRouter', () => {
|
||||
const logger = winston.createLogger();
|
||||
|
||||
it('gives status 200 when using default path', async () => {
|
||||
const app = express();
|
||||
app.use('', await createStatusCheckRouter({ logger }));
|
||||
|
||||
const response = await request(app).get('/healthcheck');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
|
||||
});
|
||||
|
||||
it('gives status 200 when using custom path', async () => {
|
||||
const app = express();
|
||||
app.use('', await createStatusCheckRouter({ logger, path: '/ready' }));
|
||||
|
||||
const response = await request(app).get('/ready');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.text).toBe(JSON.stringify({ status: 'ok' }));
|
||||
});
|
||||
|
||||
it('gives status 500 when status check throws an error', async () => {
|
||||
const app = express();
|
||||
const statusCheck = () => {
|
||||
throw Error('error!');
|
||||
};
|
||||
app.use('', await createStatusCheckRouter({ logger, statusCheck }));
|
||||
|
||||
const response = await request(app).get('/healthcheck');
|
||||
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { Logger } from 'winston';
|
||||
import Router from 'express-promise-router';
|
||||
import express from 'express';
|
||||
import { errorHandler, statusCheckHandler, StatusCheck } from '../middleware';
|
||||
|
||||
export interface StatusCheckRouterOptions {
|
||||
logger: Logger;
|
||||
path?: string;
|
||||
statusCheck?: StatusCheck;
|
||||
}
|
||||
|
||||
export async function createStatusCheckRouter(
|
||||
options: StatusCheckRouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const router = Router();
|
||||
const { path = '/healthcheck', statusCheck } = options;
|
||||
|
||||
router.use(path, await statusCheckHandler({ statusCheck }));
|
||||
router.use(errorHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -15,4 +15,5 @@
|
||||
*/
|
||||
|
||||
export { createServiceBuilder } from './createServiceBuilder';
|
||||
export { createStatusCheckRouter } from './createStatusCheckRouter';
|
||||
export type { ServiceBuilder } from './types';
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import cors from 'cors';
|
||||
import { Router } from 'express';
|
||||
import { Router, RequestHandler } from 'express';
|
||||
import { Server } from 'http';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
@@ -64,7 +64,7 @@ export type ServiceBuilder = {
|
||||
* @param root The root URL to bind to (e.g. "/api/function1")
|
||||
* @param router An express router
|
||||
*/
|
||||
addRouter(root: string, router: Router): ServiceBuilder;
|
||||
addRouter(root: string, router: Router | RequestHandler): ServiceBuilder;
|
||||
|
||||
/**
|
||||
* Starts the server using the given settings.
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"dockerode": "^3.2.0",
|
||||
"express": "^4.17.1",
|
||||
"knex": "^0.21.1",
|
||||
"pg": "^8.3.0",
|
||||
"sqlite3": "^4.2.0",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
} from '@backstage/backend-common';
|
||||
import { ConfigReader, AppConfig } from '@backstage/config';
|
||||
import { loadConfig } from '@backstage/config-loader';
|
||||
import knex from 'knex';
|
||||
import knex, { PgConnectionConfig } from 'knex';
|
||||
import healthcheck from './plugins/healthcheck';
|
||||
import auth from './plugins/auth';
|
||||
import catalog from './plugins/catalog';
|
||||
import identity from './plugins/identity';
|
||||
@@ -46,11 +47,36 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
const logger = getRootLogger().child({ type: 'plugin', plugin });
|
||||
const database = knex({
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
// Supported DBs are sqlite and postgres
|
||||
const isPg = [
|
||||
'POSTGRES_USER',
|
||||
'POSTGRES_HOST',
|
||||
'POSTGRES_PASSWORD',
|
||||
].every(key => config.getOptional(`backend.${key}`));
|
||||
|
||||
let knexConfig;
|
||||
|
||||
if (isPg) {
|
||||
knexConfig = {
|
||||
client: 'pg',
|
||||
useNullAsDefault: true,
|
||||
connection: {
|
||||
port: config.getOptionalNumber('backend.POSTGRES_PORT'),
|
||||
host: config.getString('backend.POSTGRES_HOST'),
|
||||
user: config.getString('backend.POSTGRES_USER'),
|
||||
password: config.getString('backend.POSTGRES_PASSWORD'),
|
||||
database: `backstage_plugin_${plugin}`,
|
||||
} as PgConnectionConfig,
|
||||
};
|
||||
} else {
|
||||
knexConfig = {
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
};
|
||||
}
|
||||
|
||||
const database = knex(knexConfig);
|
||||
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
@@ -59,10 +85,11 @@ function makeCreateEnv(loadedConfigs: AppConfig[]) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const configs = await loadConfig();
|
||||
const configs = await loadConfig({ shouldReadSecrets: true });
|
||||
const configReader = ConfigReader.fromConfigs(configs);
|
||||
const createEnv = makeCreateEnv(configs);
|
||||
|
||||
const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck'));
|
||||
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
|
||||
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
|
||||
const authEnv = useHotMemoize(module, () => createEnv('auth'));
|
||||
@@ -75,6 +102,7 @@ async function main() {
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(configReader)
|
||||
.addRouter('', await healthcheck(healthcheckEnv))
|
||||
.addRouter('/catalog', await catalog(catalogEnv))
|
||||
.addRouter('/rollbar', await rollbar(rollbarEnv))
|
||||
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { createStatusCheckRouter } from '@backstage/backend-common';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createRouter({ logger }: PluginEnvironment) {
|
||||
return await createStatusCheckRouter({ logger, path: '/healthcheck' });
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.16",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/lodash": "^4.14.151",
|
||||
"yaml": "^1.9.2"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
const os = require('os');
|
||||
const fs = require('fs-extra');
|
||||
const fetch = require('node-fetch');
|
||||
const killTree = require('tree-kill');
|
||||
const { resolve: resolvePath, join: joinPath } = require('path');
|
||||
const Browser = require('zombie');
|
||||
@@ -28,6 +29,7 @@ const {
|
||||
waitForExit,
|
||||
print,
|
||||
} = require('./helpers');
|
||||
const pgtools = require('pgtools');
|
||||
|
||||
async function main() {
|
||||
const rootDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-e2e-'));
|
||||
@@ -36,8 +38,9 @@ async function main() {
|
||||
print('Building dist workspace');
|
||||
const workspaceDir = await buildDistWorkspace('workspace', rootDir);
|
||||
|
||||
const isPostgres = Boolean(process.env.POSTGRES_USER);
|
||||
print('Creating a Backstage App');
|
||||
const appDir = await createApp('test-app', workspaceDir, rootDir);
|
||||
const appDir = await createApp('test-app', isPostgres, workspaceDir, rootDir);
|
||||
|
||||
print('Creating a Backstage Plugin');
|
||||
const pluginName = await createPlugin('test-plugin', appDir);
|
||||
@@ -45,6 +48,9 @@ async function main() {
|
||||
print('Starting the app');
|
||||
await testAppServe(pluginName, appDir);
|
||||
|
||||
print('Testing the backend startup');
|
||||
await testBackendStart(appDir, isPostgres);
|
||||
|
||||
print('All tests successful, removing test dir');
|
||||
await fs.remove(rootDir);
|
||||
}
|
||||
@@ -97,7 +103,7 @@ async function pinYarnVersion(dir) {
|
||||
/**
|
||||
* Creates a new app inside rootDir called test-app, using packages from the workspaceDir
|
||||
*/
|
||||
async function createApp(appName, workspaceDir, rootDir) {
|
||||
async function createApp(appName, isPostgres, workspaceDir, rootDir) {
|
||||
const child = spawnPiped(
|
||||
[
|
||||
'node',
|
||||
@@ -119,6 +125,14 @@ async function createApp(appName, workspaceDir, rootDir) {
|
||||
await waitFor(() => stdout.includes('Enter a name for the app'));
|
||||
child.stdin.write(`${appName}\n`);
|
||||
|
||||
await waitFor(() => stdout.includes('Select database for the backend'));
|
||||
|
||||
if (!isPostgres) {
|
||||
// Simulate down arrow press
|
||||
child.stdin.write(`\u001B\u005B\u0042`);
|
||||
}
|
||||
child.stdin.write(`\n`);
|
||||
|
||||
print('Waiting for app create script to be done');
|
||||
await waitForExit(child);
|
||||
|
||||
@@ -201,7 +215,7 @@ async function createPlugin(pluginName, appDir) {
|
||||
await waitForExit(child);
|
||||
|
||||
const pluginDir = resolvePath(appDir, 'plugins', pluginName);
|
||||
for (const cmd of [['lint'], ['test', '--no-watch']]) {
|
||||
for (const cmd of [['tsc'], ['lint'], ['test', '--no-watch']]) {
|
||||
print(`Running 'yarn ${cmd.join(' ')}' in newly created plugin`);
|
||||
await runPlain(['yarn', ...cmd], { cwd: pluginDir });
|
||||
}
|
||||
@@ -250,5 +264,87 @@ async function testAppServe(pluginName, appDir) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates PG databases (drops if exists before) */
|
||||
async function createDB(database) {
|
||||
const config = {
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: process.env.POSTGRES_PORT,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
};
|
||||
|
||||
try {
|
||||
await pgtools.dropdb({ config }, database);
|
||||
} catch (_) {
|
||||
/* do nothing*/
|
||||
}
|
||||
return pgtools.createdb(config, database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start serving the newly created backend and make sure that all db migrations works correctly
|
||||
*/
|
||||
async function testBackendStart(appDir, isPostgres) {
|
||||
if (isPostgres) {
|
||||
print('Creating DBs');
|
||||
await Promise.all(
|
||||
[
|
||||
'catalog',
|
||||
'scaffolder',
|
||||
'auth',
|
||||
'identity',
|
||||
'proxy',
|
||||
'techdocs',
|
||||
].map(name => createDB(`backstage_plugin_${name}`)),
|
||||
);
|
||||
print('Created DBs');
|
||||
}
|
||||
|
||||
const child = spawnPiped(['yarn', 'workspace', 'backend', 'start'], {
|
||||
cwd: appDir,
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', data => {
|
||||
stdout = stdout + data.toString('utf8');
|
||||
});
|
||||
child.stderr.on('data', data => {
|
||||
stderr = stderr + data.toString('utf8');
|
||||
});
|
||||
let successful = false;
|
||||
|
||||
try {
|
||||
await waitFor(() => stdout.includes('Listening on ') || stderr !== '');
|
||||
if (stderr !== '') {
|
||||
// Skipping the whole block
|
||||
throw new Error(stderr);
|
||||
}
|
||||
|
||||
print('Try to fetch entities from the backend');
|
||||
// Try fetch entities, should be ok
|
||||
await fetch('http://localhost:7000/catalog/entities').then(res =>
|
||||
res.json(),
|
||||
);
|
||||
print('Entities fetched successfully');
|
||||
successful = true;
|
||||
} catch (error) {
|
||||
throw new Error(`Backend failed to startup: ${error}`);
|
||||
} finally {
|
||||
print('Stopping the child process');
|
||||
// Kill entire process group, otherwise we'll end up with hanging serve processes
|
||||
killTree(child.pid);
|
||||
}
|
||||
|
||||
try {
|
||||
await waitForExit(child);
|
||||
} catch (error) {
|
||||
if (!successful) {
|
||||
throw new Error(`Backend failed to startup: ${stderr}`);
|
||||
}
|
||||
print('Backend startup test finished successfully');
|
||||
}
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', handleError);
|
||||
main(process.argv.slice(2)).catch(handleError);
|
||||
|
||||
@@ -76,6 +76,12 @@ function handleError(err) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for fn() to be true
|
||||
* Checks every 100ms
|
||||
* .cancel() is available
|
||||
* @returns {Promise} Promise of resolution
|
||||
*/
|
||||
function waitFor(fn) {
|
||||
return new Promise(resolve => {
|
||||
const handle = setInterval(() => {
|
||||
|
||||
@@ -68,7 +68,9 @@
|
||||
"jest-css-modules": "^2.1.0",
|
||||
"jest-esm-transformer": "^1.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-fetch": "^2.6.0",
|
||||
"ora": "^4.0.3",
|
||||
"pgtools": "^0.3.0",
|
||||
"raw-loader": "^4.0.1",
|
||||
"react": "^16.0.0",
|
||||
"react-dev-utils": "^10.2.1",
|
||||
|
||||
@@ -104,8 +104,17 @@ export default async (cmd: Command): Promise<void> => {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'dbType',
|
||||
message: chalk.blue('Select database for the backend [required]'),
|
||||
// @ts-ignore
|
||||
choices: ['PostgreSQL', 'SQLite'],
|
||||
},
|
||||
];
|
||||
const answers: Answers = await inquirer.prompt(questions);
|
||||
answers.dbTypePG = answers.dbType === 'PostgreSQL';
|
||||
answers.dbTypeSqlite = answers.dbType === 'SQLite';
|
||||
|
||||
const templateDir = paths.resolveOwn('templates/default-app');
|
||||
const tempDir = resolvePath(os.tmpdir(), answers.name);
|
||||
|
||||
@@ -134,11 +134,15 @@ async function findTargetPackages(pkgNames: string[]): Promise<LernaPackage[]> {
|
||||
throw new Error(`Package '${name}' not found`);
|
||||
}
|
||||
|
||||
const pkgDeps = Object.keys(node.pkg.dependencies);
|
||||
const localDeps: string[] = Array.from(node.localDependencies.keys());
|
||||
const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep));
|
||||
// Don't include dependencies of packages that are marked as bundled
|
||||
if (!node.pkg.get('bundled')) {
|
||||
const pkgDeps = Object.keys(node.pkg.dependencies);
|
||||
const localDeps: string[] = Array.from(node.localDependencies.keys());
|
||||
const filteredDeps = localDeps.filter(dep => pkgDeps.includes(dep));
|
||||
|
||||
searchNames.push(...filteredDeps);
|
||||
}
|
||||
|
||||
searchNames.push(...filteredDeps);
|
||||
targets.set(name, node.pkg);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function runCheck(cmd: string, ...args: string[]) {
|
||||
}
|
||||
|
||||
export async function waitForExit(
|
||||
child: ChildProcess & { exitCode?: number },
|
||||
child: ChildProcess & { exitCode: number | null },
|
||||
name?: string,
|
||||
): Promise<void> {
|
||||
if (typeof child.exitCode === 'number') {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
root: true,
|
||||
};
|
||||
|
||||
@@ -4,3 +4,19 @@ app:
|
||||
|
||||
organization:
|
||||
name: Acme Corporation
|
||||
|
||||
backend:
|
||||
baseUrl: http://localhost:7000
|
||||
listen: 0.0.0.0:7000
|
||||
cors:
|
||||
origin: http://localhost:3000
|
||||
methods: [GET, POST, PUT, DELETE]
|
||||
credentials: true
|
||||
|
||||
proxy:
|
||||
'/test':
|
||||
target: 'https://example.com'
|
||||
changeOrigin: true
|
||||
|
||||
techdocs:
|
||||
storageUrl: https://techdocs-mock-sites.storage.googleapis.com
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"cross-env": "^7.0.0",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM node:12
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn install --frozen-lockfile --production
|
||||
|
||||
CMD ["node", "packages/backend"]
|
||||
@@ -0,0 +1,68 @@
|
||||
# example-backend
|
||||
|
||||
This package is an EXAMPLE of a Backstage backend.
|
||||
|
||||
The main purpose of this package is to provide a test bed for Backstage plugins
|
||||
that have a backend part. Feel free to experiment locally or within your fork
|
||||
by adding dependencies and routes to this backend, to try things out.
|
||||
|
||||
Our goal is to eventually amend the create-app flow of the CLI, such that a
|
||||
production ready version of a backend skeleton is made alongside the frontend
|
||||
app. Until then, feel free to experiment here!
|
||||
|
||||
## Development
|
||||
|
||||
To run the example backend, first go to the project root and run
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn tsc
|
||||
yarn build
|
||||
```
|
||||
|
||||
You should only need to do this once.
|
||||
|
||||
After that, go to the `packages/backend` directory and run
|
||||
|
||||
```bash
|
||||
AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \
|
||||
AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \
|
||||
AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \
|
||||
AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \
|
||||
LOG_LEVEL=debug \
|
||||
yarn start
|
||||
```
|
||||
|
||||
Substitute `x` for actual values, or leave them as
|
||||
dummy values just to try out the backend without using the auth or sentry features.
|
||||
|
||||
The backend starts up on port 7000 per default.
|
||||
|
||||
## Populating The Catalog
|
||||
|
||||
If you want to use the catalog functionality, you need to add so called locations
|
||||
to the backend. These are places where the backend can find some entity descriptor
|
||||
data to consume and serve.
|
||||
|
||||
To get started, you can issue the following after starting the backend, from inside
|
||||
the `plugins/catalog-backend` directory:
|
||||
|
||||
```bash
|
||||
yarn mock-data
|
||||
```
|
||||
|
||||
You should then start seeing data on `localhost:7000/catalog/entities`.
|
||||
|
||||
The catalog currently runs in-memory only, so feel free to try it out, but it will
|
||||
need to be re-populated on next startup.
|
||||
|
||||
## Authentication
|
||||
|
||||
We chose [Passport](http://www.passportjs.org/) as authentication platform due to its comprehensive set of supported authentication [strategies](http://www.passportjs.org/packages/).
|
||||
|
||||
Read more about the [auth-backend](https://github.com/spotify/backstage/blob/master/plugins/auth-backend/README.md) and [how to add a new provider](https://github.com/spotify/backstage/blob/master/docs/auth/add-auth-provider.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md)
|
||||
- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md)
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "12"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "backstage-cli backend:build",
|
||||
"build-image": "backstage-cli backend:build-image example-backend",
|
||||
"start": "backstage-cli backend:dev",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"clean": "backstage-cli clean",
|
||||
"migrate:create": "knex migrate:make -x ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^{{version}}",
|
||||
"@backstage/catalog-model": "^{{version}}",
|
||||
"@backstage/config": "^0.1.1-alpha.13",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.13",
|
||||
"@backstage/plugin-auth-backend": "^{{version}}",
|
||||
"@backstage/plugin-catalog-backend": "^{{version}}",
|
||||
"@backstage/plugin-identity-backend": "^{{version}}",
|
||||
"@backstage/plugin-proxy-backend": "^{{version}}",
|
||||
"@backstage/plugin-rollbar-backend": "^{{version}}",
|
||||
"@backstage/plugin-scaffolder-backend": "^{{version}}",
|
||||
"@backstage/plugin-sentry-backend": "^{{version}}",
|
||||
"@backstage/plugin-techdocs-backend": "^{{version}}",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
"dockerode": "^3.2.0",
|
||||
"express": "^4.17.1",
|
||||
"knex": "^0.21.1",
|
||||
{{#if dbTypePG}}
|
||||
"pg": "^8.3.0",
|
||||
{{/if}}
|
||||
{{#if dbTypeSqlite}}
|
||||
"sqlite3": "^4.2.0",
|
||||
{{/if}}
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.15",
|
||||
"@types/dockerode": "^2.5.32",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-serve-static-core": "^4.17.5",
|
||||
"@types/helmet": "^0.0.47"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { PluginEnvironment } from './types';
|
||||
|
||||
describe('test', () => {
|
||||
it('unbreaks the test runner', () => {
|
||||
const unbreaker = {} as PluginEnvironment;
|
||||
expect(unbreaker).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Hi!
|
||||
*
|
||||
* Note that this is an EXAMPLE Backstage backend. Please check the README.
|
||||
*
|
||||
* Happy hacking!
|
||||
*/
|
||||
|
||||
import {
|
||||
createServiceBuilder,
|
||||
getRootLogger,
|
||||
useHotMemoize,
|
||||
} from '@backstage/backend-common';
|
||||
import { ConfigReader, AppConfig } from '@backstage/config';
|
||||
import { loadConfig } from '@backstage/config-loader';
|
||||
{{#if dbTypePG}}
|
||||
import knex, { PgConnectionConfig } from 'knex';
|
||||
{{/if}}
|
||||
{{#if dbTypeSqlite}}
|
||||
import knex from 'knex';
|
||||
{{/if}}
|
||||
import auth from './plugins/auth';
|
||||
import catalog from './plugins/catalog';
|
||||
import identity from './plugins/identity';
|
||||
import scaffolder from './plugins/scaffolder';
|
||||
import proxy from './plugins/proxy';
|
||||
import techdocs from './plugins/techdocs';
|
||||
import { PluginEnvironment } from './types';
|
||||
|
||||
function makeCreateEnv(loadedConfigs: AppConfig[]) {
|
||||
const config = ConfigReader.fromConfigs(loadedConfigs);
|
||||
|
||||
return (plugin: string): PluginEnvironment => {
|
||||
const logger = getRootLogger().child({ type: 'plugin', plugin });
|
||||
|
||||
{{#if dbTypePG}}
|
||||
const knexConfig = {
|
||||
client: 'pg',
|
||||
useNullAsDefault: true,
|
||||
connection: {
|
||||
port: process.env.POSTGRES_PORT,
|
||||
host: process.env.POSTGRES_HOST,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: `backstage_plugin_${plugin}`,
|
||||
} as PgConnectionConfig,
|
||||
};
|
||||
{{/if}}
|
||||
{{#if dbTypeSqlite}}
|
||||
const knexConfig = {
|
||||
client: 'sqlite3',
|
||||
connection: ':memory:',
|
||||
useNullAsDefault: true,
|
||||
};
|
||||
{{/if}}
|
||||
const database = knex(knexConfig);
|
||||
database.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
|
||||
resource.run('PRAGMA foreign_keys = ON', () => {});
|
||||
});
|
||||
return { logger, database, config };
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const configs = await loadConfig();
|
||||
const configReader = ConfigReader.fromConfigs(configs);
|
||||
const createEnv = makeCreateEnv(configs);
|
||||
|
||||
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
|
||||
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
|
||||
const authEnv = useHotMemoize(module, () => createEnv('auth'));
|
||||
const identityEnv = useHotMemoize(module, () => createEnv('identity'));
|
||||
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
|
||||
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
|
||||
|
||||
const service = createServiceBuilder(module)
|
||||
.loadConfig(configReader)
|
||||
.addRouter('/catalog', await catalog(catalogEnv))
|
||||
.addRouter('/scaffolder', await scaffolder(scaffolderEnv))
|
||||
.addRouter('/auth', await auth(authEnv))
|
||||
.addRouter('/identity', await identity(identityEnv))
|
||||
.addRouter('/techdocs', await techdocs(techdocsEnv))
|
||||
.addRouter('/proxy', await proxy(proxyEnv));
|
||||
|
||||
await service.start().catch(err => {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.hot?.accept();
|
||||
main().catch(error => {
|
||||
console.error(`Backend failed to start up, ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 { createRouter } from '@backstage/plugin-auth-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
database,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config, database });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 {
|
||||
createRouter,
|
||||
DatabaseEntitiesCatalog,
|
||||
DatabaseLocationsCatalog,
|
||||
DatabaseManager,
|
||||
HigherOrderOperations,
|
||||
LocationReaders,
|
||||
runPeriodically,
|
||||
} from '@backstage/plugin-catalog-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
import { useHotCleanup } from '@backstage/backend-common';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
database,
|
||||
}: PluginEnvironment) {
|
||||
const locationReader = new LocationReaders(logger);
|
||||
|
||||
const db = await DatabaseManager.createDatabase(database, { logger });
|
||||
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
|
||||
const locationsCatalog = new DatabaseLocationsCatalog(db);
|
||||
const higherOrderOperation = new HigherOrderOperations(
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
locationReader,
|
||||
logger,
|
||||
);
|
||||
|
||||
useHotCleanup(
|
||||
module,
|
||||
runPeriodically(() => higherOrderOperation.refreshAllLocations(), 10000),
|
||||
);
|
||||
|
||||
return await createRouter({
|
||||
entitiesCatalog,
|
||||
locationsCatalog,
|
||||
higherOrderOperation,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { createRouter } from '@backstage/plugin-identity-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
return await createRouter({ logger });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
// @ts-ignore
|
||||
import { createRouter } from '@backstage/plugin-proxy-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({
|
||||
logger,
|
||||
config,
|
||||
}: PluginEnvironment) {
|
||||
return await createRouter({ logger, config });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 {
|
||||
CookieCutter,
|
||||
createRouter,
|
||||
FilePreparer,
|
||||
GithubPreparer,
|
||||
Preparers,
|
||||
GithubPublisher,
|
||||
CreateReactAppTemplater,
|
||||
Templaters,
|
||||
} from '@backstage/plugin-scaffolder-backend';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import type { PluginEnvironment } from '../types';
|
||||
import Docker from 'dockerode';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
const cookiecutterTemplater = new CookieCutter();
|
||||
const craTemplater = new CreateReactAppTemplater();
|
||||
const templaters = new Templaters();
|
||||
templaters.register('cookiecutter', cookiecutterTemplater);
|
||||
templaters.register('cra', craTemplater);
|
||||
|
||||
const filePreparer = new FilePreparer();
|
||||
const githubPreparer = new GithubPreparer();
|
||||
const preparers = new Preparers();
|
||||
|
||||
preparers.register('file', filePreparer);
|
||||
preparers.register('github', githubPreparer);
|
||||
|
||||
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
|
||||
const publisher = new GithubPublisher({ client: githubClient });
|
||||
|
||||
const dockerClient = new Docker();
|
||||
return await createRouter({
|
||||
preparers,
|
||||
templaters,
|
||||
publisher,
|
||||
logger,
|
||||
dockerClient,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 { createRouter } from '@backstage/plugin-techdocs-backend';
|
||||
import { PluginEnvironment } from '../types';
|
||||
|
||||
export default async function createPlugin({ logger }: PluginEnvironment) {
|
||||
return await createRouter({ logger });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 Knex from 'knex';
|
||||
import { Logger } from 'winston';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export type PluginEnvironment = {
|
||||
logger: Logger;
|
||||
database: Knex;
|
||||
config: Config;
|
||||
};
|
||||
@@ -1,8 +1,14 @@
|
||||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": ["packages/*/src", "plugins/*/src", "plugins/*/dev"],
|
||||
"exclude": ["**/node_modules"],
|
||||
"include": [
|
||||
"packages/*/src",
|
||||
"plugins/*/src",
|
||||
"plugins/*/dev",
|
||||
"plugins/*/migrations"
|
||||
],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"yup": "^0.29.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/yup": "^0.28.2"
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"lodash": "^4.17.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^10.4.1",
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"jest-fetch-mock": "^3.0.3"
|
||||
|
||||
@@ -17,7 +17,7 @@ import { createApiRef } from '../ApiRef';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
// Using interface to make the ConfigApi name show up in docs
|
||||
export interface ConfigApi extends Config {}
|
||||
export type ConfigApi = Config;
|
||||
|
||||
export const configApiRef = createApiRef<ConfigApi>({
|
||||
id: 'core.config',
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"@testing-library/user-event": "^12.0.7",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-helmet": "^5.0.15",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
oauth2ApiRef,
|
||||
oktaAuthApiRef,
|
||||
useApi,
|
||||
configApiRef,
|
||||
} from '@backstage/core-api';
|
||||
import Collapse from '@material-ui/core/Collapse';
|
||||
import SignOutIcon from '@material-ui/icons/MeetingRoom';
|
||||
@@ -39,6 +40,9 @@ export function SidebarUserSettings() {
|
||||
const { isOpen: sidebarOpen } = useContext(SidebarContext);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const identityApi = useApi(identityApiRef);
|
||||
const configApi = useApi(configApiRef);
|
||||
const providersConfig = configApi.getOptionalConfig('auth.providers');
|
||||
const providers = providersConfig?.keys() ?? [];
|
||||
|
||||
// Close the provider list when sidebar collapse
|
||||
useEffect(() => {
|
||||
@@ -49,31 +53,41 @@ export function SidebarUserSettings() {
|
||||
<>
|
||||
<SidebarUserProfile open={open} setOpen={setOpen} />
|
||||
<Collapse in={open} timeout="auto">
|
||||
<OIDCProviderSettings
|
||||
title="Google"
|
||||
apiRef={googleAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
<OAuthProviderSettings
|
||||
title="Github"
|
||||
apiRef={githubAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
<OAuthProviderSettings
|
||||
title="Gitlab"
|
||||
apiRef={gitlabAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
<OIDCProviderSettings
|
||||
title="Okta"
|
||||
apiRef={oktaAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
<OIDCProviderSettings
|
||||
title="YourOrg"
|
||||
apiRef={oauth2ApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
{providers.includes('google') && (
|
||||
<OIDCProviderSettings
|
||||
title="Google"
|
||||
apiRef={googleAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('github') && (
|
||||
<OAuthProviderSettings
|
||||
title="Github"
|
||||
apiRef={githubAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('gitlab') && (
|
||||
<OAuthProviderSettings
|
||||
title="Gitlab"
|
||||
apiRef={gitlabAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('okta') && (
|
||||
<OIDCProviderSettings
|
||||
title="Okta"
|
||||
apiRef={oktaAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{providers.includes('oauth2') && (
|
||||
<OIDCProviderSettings
|
||||
title="YourOrg"
|
||||
apiRef={oauth2ApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
<SidebarItem
|
||||
icon={SignOutIcon}
|
||||
text="Sign Out"
|
||||
|
||||
@@ -33,19 +33,13 @@ import { SignInProvider } from './types';
|
||||
const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider';
|
||||
|
||||
// Separate list here to avoid exporting internal types
|
||||
export type SignInProviderId =
|
||||
| 'guest'
|
||||
| 'google'
|
||||
| 'gitlab'
|
||||
| 'custom'
|
||||
| 'okta'
|
||||
| 'github';
|
||||
export type SignInProviderId = 'guest' | string;
|
||||
|
||||
const signInProviders: { [id in SignInProviderId]: SignInProvider } = {
|
||||
guest: guestProvider,
|
||||
google: googleProvider,
|
||||
gitlab: gitlabProvider,
|
||||
custom: customProvider,
|
||||
oauth2: customProvider,
|
||||
okta: oktaProvider,
|
||||
github: githubProvider,
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"react-router-dom": "6.0.0-beta.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
|
||||
rules: {
|
||||
'no-console': 0,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
# DocGen - API Reference Documentation Generator
|
||||
|
||||
The docgen package provides a CLI to generate markdown documentation for all exported ApiRefs in `@backstage/core`. The documentation is generated based on exported `ApiRef` instances and their type parameters.
|
||||
|
||||
The CLI supports generating both TechDocs and GitHub Markdown, where the TechDocs one provides some better linking and syntax highlighting.
|
||||
|
||||
## Usage
|
||||
|
||||
To generate markdown documentation in the top-level `docs/` directory, run the following:
|
||||
|
||||
```bash
|
||||
yarn docgen
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
This package was lifted out from the Spotify internal Backstage project and could use some further work:
|
||||
|
||||
- Use a higher-level TypeScript compiler library, e.g. `ts-morph`.
|
||||
- Support for generating docs for any package or multiple packages.
|
||||
- Better handling of self-referencing types in APIs, e.g. ConfigApi.
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// Figure out whether we're running inside the backstage repo or as an installed dependency
|
||||
const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src'));
|
||||
|
||||
if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) {
|
||||
require('..');
|
||||
} else {
|
||||
require('ts-node').register({
|
||||
transpileOnly: true,
|
||||
compilerOptions: {
|
||||
module: 'CommonJS',
|
||||
},
|
||||
});
|
||||
|
||||
require('../src');
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "docgen",
|
||||
"description": "Tool for generating API Documentation for itself",
|
||||
"version": "0.1.1-alpha.16",
|
||||
"private": true,
|
||||
"homepage": "https://backstage.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/spotify/backstage",
|
||||
"directory": "packages/docgen"
|
||||
},
|
||||
"keywords": [
|
||||
"backstage"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli build --outputs cjs",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"docgen": "backstage-docgen generate --output ../../docs/reference/utility-apis --format github && prettier --write ../../docs/reference/utility-apis",
|
||||
"clean": "backstage-cli clean",
|
||||
"start": "nodemon --"
|
||||
},
|
||||
"bin": {
|
||||
"backstage-docgen": "bin/backstage-docgen"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"commander": "^4.1.1",
|
||||
"fs-extra": "^9.0.0",
|
||||
"github-slugger": "^1.3.0",
|
||||
"ts-node": "^8.6.2",
|
||||
"typescript": "^3.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/node": "^13.7.2",
|
||||
"nodemon": "^2.0.2"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"nodemonConfig": {
|
||||
"watch": "./src",
|
||||
"exec": "bin/backstage-docgen",
|
||||
"ext": "ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* 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 ts from 'typescript';
|
||||
import TypeLocator from './TypeLocator';
|
||||
import { createMemProgram } from './testUtils';
|
||||
import ApiDocGenerator from './ApiDocGenerator';
|
||||
|
||||
describe('ApiDocGenerator', () => {
|
||||
it('should generate empty API doc', () => {
|
||||
const program = createMemProgram(
|
||||
`
|
||||
import MyApi from './type';
|
||||
|
||||
type MyApiType = {};
|
||||
|
||||
export const myApi = new MyApi<MyApiType>({
|
||||
id: 'my-id',
|
||||
description: 'my-description',
|
||||
});
|
||||
`,
|
||||
{
|
||||
'/mem/type.ts': `export default class MyApi<T> {
|
||||
constructor(private readonly info: { id: string, description: string }) {}
|
||||
}`,
|
||||
},
|
||||
);
|
||||
|
||||
const typeLocator = TypeLocator.fromProgram(program, '/');
|
||||
|
||||
const { apiInstances } = typeLocator.findExportedInstances({
|
||||
apiInstances: typeLocator.getExportedType('/mem/type.ts'),
|
||||
});
|
||||
|
||||
expect(apiInstances.length).toBe(1);
|
||||
const [apiInstance] = apiInstances;
|
||||
|
||||
const docGenerator = ApiDocGenerator.fromProgram(program, '/');
|
||||
const doc = docGenerator.toDoc(apiInstance);
|
||||
|
||||
expect(doc.id).toBe('my-id');
|
||||
expect(doc.description).toBe('my-description');
|
||||
expect(doc.name).toBe('myApi');
|
||||
expect(doc.file).toBe('mem/index.ts');
|
||||
expect(doc.lineInFile).toBe(6);
|
||||
expect(doc.interfaceInfos).toEqual([
|
||||
{
|
||||
dependentTypes: [],
|
||||
docs: [],
|
||||
file: 'mem/index.ts',
|
||||
lineInFile: 4,
|
||||
members: [],
|
||||
name: 'MyApiType',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate API docs', () => {
|
||||
const program = createMemProgram(
|
||||
`
|
||||
import MyApi from './type';
|
||||
|
||||
/** MySubSubType Docs */
|
||||
type MySubSubType = { n: number; };
|
||||
|
||||
/** MySubType Docs */
|
||||
type MySubType = {
|
||||
/** Field a docs */
|
||||
a: boolean;
|
||||
// Field b docs
|
||||
b: MySubSubType;
|
||||
}
|
||||
|
||||
/** MySecondSubType Docs */
|
||||
|
||||
/** With multiple comments */
|
||||
export type MySecondSubType = { s: string };
|
||||
|
||||
// MyThirdSubType Docs that shouldn't show up
|
||||
type MyThirdSubType = { b: boolean };
|
||||
|
||||
/** MyApiType Docs */
|
||||
type MyApiType = {
|
||||
/** Docs for x */
|
||||
x: string;
|
||||
// Line comments shouldn't show up
|
||||
y: MySubType;
|
||||
/** Multiple */
|
||||
/** JsDoc */
|
||||
/** Comments */
|
||||
z(a: Promise<readonly [{k: MySecondSubType}[]]>): Array<MyThirdSubType>;
|
||||
};
|
||||
|
||||
/** Should not show up */
|
||||
export const myApi = new MyApi<MyApiType>({
|
||||
id: 'my-id',
|
||||
description: 'my-description',
|
||||
});
|
||||
`,
|
||||
{
|
||||
'/mem/type.ts': `export default class MyApi<T> {
|
||||
constructor(private readonly info: { id: string, description: string }) {}
|
||||
}`,
|
||||
},
|
||||
);
|
||||
|
||||
const source = program.getSourceFile('/mem/index.ts');
|
||||
|
||||
// Figure out type IDs so we can make sure they match later
|
||||
const checker = program.getTypeChecker();
|
||||
const symbols = checker.getSymbolsInScope(
|
||||
source!.getChildren().slice(-1)[0],
|
||||
ts.SymbolFlags.TypeAlias,
|
||||
);
|
||||
const Ids = [
|
||||
'MySubType',
|
||||
'MySubSubType',
|
||||
'MySecondSubType',
|
||||
'MyThirdSubType',
|
||||
].reduce((ids, name) => {
|
||||
const symbol = symbols.find(s => s.escapedName === name)!;
|
||||
const type = checker.getTypeAtLocation(symbol.declarations[0]);
|
||||
ids[name] = (type.aliasSymbol as any).id;
|
||||
return ids;
|
||||
}, {} as { [key in string]: number });
|
||||
|
||||
const typeLocator = TypeLocator.fromProgram(program, '/mem');
|
||||
|
||||
const { apiInstances } = typeLocator.findExportedInstances({
|
||||
apiInstances: typeLocator.getExportedType('/mem/type.ts'),
|
||||
});
|
||||
|
||||
expect(apiInstances.length).toBe(1);
|
||||
const [apiInstance] = apiInstances;
|
||||
|
||||
const docGenerator = ApiDocGenerator.fromProgram(program, '/mem');
|
||||
const doc = docGenerator.toDoc(apiInstance);
|
||||
|
||||
expect(doc.id).toBe('my-id');
|
||||
expect(doc.description).toBe('my-description');
|
||||
expect(doc.name).toBe('myApi');
|
||||
expect(doc.file).toBe('index.ts');
|
||||
expect(doc.interfaceInfos.length).toBe(1);
|
||||
expect(doc.interfaceInfos[0].docs).toEqual(['MyApiType Docs']);
|
||||
expect(doc.interfaceInfos[0].file).toBe('index.ts');
|
||||
expect(doc.interfaceInfos[0].lineInFile).toBe(24);
|
||||
expect(doc.interfaceInfos[0].name).toBe('MyApiType');
|
||||
expect(doc.interfaceInfos[0].members).toEqual([
|
||||
{
|
||||
type: 'prop',
|
||||
name: 'x',
|
||||
path: 'MyApiType.x',
|
||||
text: 'x: string',
|
||||
docs: ['Docs for x'],
|
||||
links: [],
|
||||
},
|
||||
{
|
||||
type: 'prop',
|
||||
name: 'y',
|
||||
path: 'MyApiType.y',
|
||||
text: 'y: MySubType',
|
||||
docs: [],
|
||||
links: [
|
||||
{
|
||||
id: Ids.MySubType,
|
||||
name: 'MySubType',
|
||||
path: 'index.ts/MySubType',
|
||||
location: [3, 12],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'method',
|
||||
name: 'z',
|
||||
path: 'MyApiType.z',
|
||||
text:
|
||||
'z(a: Promise<readonly [{k: MySecondSubType}[]]>): Array<MyThirdSubType>',
|
||||
docs: ['Multiple', 'JsDoc', 'Comments'],
|
||||
links: [
|
||||
{
|
||||
id: Ids.MySecondSubType,
|
||||
name: 'MySecondSubType',
|
||||
path: 'index.ts/MySecondSubType',
|
||||
location: [27, 42],
|
||||
},
|
||||
{
|
||||
id: Ids.MyThirdSubType,
|
||||
name: 'MyThirdSubType',
|
||||
path: 'index.ts/MyThirdSubType',
|
||||
location: [56, 70],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(doc.interfaceInfos[0].dependentTypes).toEqual([
|
||||
{
|
||||
id: Ids.MySubType,
|
||||
name: 'MySubType',
|
||||
path: 'index.ts/MySubType',
|
||||
file: 'index.ts',
|
||||
lineInFile: 8,
|
||||
text: `type MySubType = {
|
||||
/** Field a docs */
|
||||
a: boolean;
|
||||
// Field b docs
|
||||
b: MySubSubType;
|
||||
}`,
|
||||
docs: ['MySubType Docs'],
|
||||
links: [
|
||||
{
|
||||
id: Ids.MySubSubType,
|
||||
name: 'MySubSubType',
|
||||
path: 'index.ts/MySubSubType',
|
||||
location: [102, 114],
|
||||
},
|
||||
],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: Ids.MySubSubType,
|
||||
name: 'MySubSubType',
|
||||
path: 'index.ts/MySubSubType',
|
||||
file: 'index.ts',
|
||||
lineInFile: 5,
|
||||
text: 'type MySubSubType = { n: number; }',
|
||||
docs: ['MySubSubType Docs'],
|
||||
links: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: Ids.MySecondSubType,
|
||||
name: 'MySecondSubType',
|
||||
path: 'index.ts/MySecondSubType',
|
||||
file: 'index.ts',
|
||||
lineInFile: 18,
|
||||
text: 'export type MySecondSubType = { s: string }',
|
||||
docs: ['MySecondSubType Docs', 'With multiple comments'],
|
||||
links: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: Ids.MyThirdSubType,
|
||||
name: 'MyThirdSubType',
|
||||
path: 'index.ts/MyThirdSubType',
|
||||
file: 'index.ts',
|
||||
lineInFile: 21,
|
||||
text: 'type MyThirdSubType = { b: boolean }',
|
||||
docs: [],
|
||||
links: [],
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* 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 ts from 'typescript';
|
||||
import { relative } from 'path';
|
||||
import {
|
||||
ExportedInstance,
|
||||
ApiDoc,
|
||||
InterfaceInfo,
|
||||
FieldInfo,
|
||||
TypeInfo,
|
||||
TypeLink,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* The ApiDocGenerator uses the typescript compiler API to build the data structure that
|
||||
* describes a Backstage API and all of it's related types.
|
||||
*
|
||||
* It receives an exported instance that of the form
|
||||
* `export name = createApiRef<Interface>({id: ..., description: ...})`.
|
||||
* It will then traverse all fields and methods on the interface type, and also
|
||||
* types and declaration of all types that are used by those, both directly and indirectly.
|
||||
* While traversing, it collects information such as names, location in source, docs, etc.
|
||||
*/
|
||||
export default class ApiDocGenerator {
|
||||
static fromProgram(program: ts.Program, sourcePath: string) {
|
||||
return new ApiDocGenerator(program.getTypeChecker(), sourcePath);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly checker: ts.TypeChecker,
|
||||
private readonly basePath: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generate documentation information for a given exported symbol.
|
||||
*/
|
||||
toDoc(apiInstance: ExportedInstance): ApiDoc {
|
||||
const { name, source, args, typeArgs } = apiInstance;
|
||||
|
||||
const [info] = args;
|
||||
if (!ts.isObjectLiteralExpression(info)) {
|
||||
throw new Error('api info is not an object literal');
|
||||
}
|
||||
|
||||
const id = this.getObjectPropertyLiteral(info, 'id');
|
||||
const description = this.getObjectPropertyLiteral(info, 'description');
|
||||
const file = relative(this.basePath, source.fileName);
|
||||
const { line } = source.getLineAndCharacterOfPosition(
|
||||
apiInstance.node.getStart(),
|
||||
);
|
||||
|
||||
const rootTypeNode = typeArgs[0];
|
||||
const typeNodes = ts.isIntersectionTypeNode(rootTypeNode)
|
||||
? rootTypeNode.types.slice()
|
||||
: [rootTypeNode];
|
||||
const interfaceInfos = typeNodes.map(typeNode =>
|
||||
this.getInterfaceInfo(typeNode),
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
file,
|
||||
lineInFile: line + 1,
|
||||
description,
|
||||
interfaceInfos,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab jsDoc and regular comments for a node
|
||||
*/
|
||||
private getNodeDocs(node: ts.Node): string[] {
|
||||
const docNodes = ((node && (node as any).jsDoc) || []) as ts.JSDoc[];
|
||||
const docs = docNodes.map(docNode => docNode.comment || '').filter(Boolean);
|
||||
return docs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect information about a top-level type.
|
||||
*/
|
||||
private getInterfaceInfo(typeNode: ts.TypeNode): InterfaceInfo {
|
||||
if (ts.isTypeQueryNode(typeNode)) {
|
||||
throw new Error(
|
||||
'APIs must have a proper type parameter, TypeQueries are now allowed',
|
||||
);
|
||||
}
|
||||
if (!ts.isTypeReferenceNode(typeNode)) {
|
||||
throw new Error('Interface is not a type node');
|
||||
}
|
||||
|
||||
const type = this.checker.getTypeFromTypeNode(typeNode);
|
||||
const interfaceMembers = type.symbol.members as Map<string, ts.Symbol>;
|
||||
if (!interfaceMembers) {
|
||||
throw new Error('Interface does not have any members');
|
||||
}
|
||||
const name = (type.aliasSymbol || type.symbol).name;
|
||||
const [declaration] = (type.aliasSymbol || type.symbol).declarations;
|
||||
const sourceFile = declaration.getSourceFile();
|
||||
const file = relative(this.basePath, sourceFile.fileName);
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(
|
||||
declaration.getStart(),
|
||||
);
|
||||
const docs = this.getNodeDocs(declaration);
|
||||
|
||||
const membersAndTypes = Array.from(
|
||||
interfaceMembers.values(),
|
||||
).flatMap(fieldSymbol => this.getMemberInfo(name, fieldSymbol));
|
||||
|
||||
const members = membersAndTypes.map(t => t.member);
|
||||
const dependentTypes = this.flattenTypes(
|
||||
membersAndTypes.flatMap(t => t.dependentTypes),
|
||||
);
|
||||
|
||||
return { name, docs, file, lineInFile: line + 1, members, dependentTypes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab primitive values from an object literal expression.
|
||||
*/
|
||||
private getObjectPropertyLiteral(
|
||||
objectLiteral: ts.ObjectLiteralExpression,
|
||||
propertyName: string,
|
||||
): string {
|
||||
const matchingProp = objectLiteral.properties.filter(
|
||||
prop =>
|
||||
ts.isPropertyAssignment(prop) &&
|
||||
ts.isIdentifier(prop.name) &&
|
||||
prop.name.text === propertyName,
|
||||
)[0] as ts.PropertyAssignment;
|
||||
|
||||
if (!matchingProp) {
|
||||
throw new Error(`no identifier found for property ${propertyName}`);
|
||||
}
|
||||
|
||||
const { initializer } = matchingProp;
|
||||
if (!ts.isStringLiteral(initializer)) {
|
||||
throw new Error(`no string literal for ${propertyName}`);
|
||||
}
|
||||
|
||||
return initializer.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field definitions for a given symbol.
|
||||
*/
|
||||
private getMemberInfo(
|
||||
parentName: string,
|
||||
symbol: ts.Symbol,
|
||||
): { member: FieldInfo; dependentTypes: TypeInfo[] } {
|
||||
const declaration = symbol.valueDeclaration;
|
||||
|
||||
const { links, infos: dependentTypes } = this.findAllTypeReferences(
|
||||
declaration,
|
||||
);
|
||||
|
||||
let type: FieldInfo['type'] = 'prop';
|
||||
if (
|
||||
ts.isPropertySignature(declaration) &&
|
||||
declaration.type &&
|
||||
ts.isFunctionTypeNode(declaration.type)
|
||||
) {
|
||||
type = 'method';
|
||||
} else if (ts.isMethodSignature(declaration)) {
|
||||
type = 'method';
|
||||
}
|
||||
|
||||
return {
|
||||
member: {
|
||||
type,
|
||||
name: symbol.name,
|
||||
path: `${parentName}.${symbol.name}`,
|
||||
text: declaration.getText().replace(/;$/, ''),
|
||||
docs: this.getNodeDocs(declaration),
|
||||
links: this.recalculateLinkOffsets(declaration, links),
|
||||
},
|
||||
dependentTypes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively search for references to types that we care about and want to include in the documentation.
|
||||
*/
|
||||
private findAllTypeReferences = (
|
||||
node: ts.Node | undefined,
|
||||
visited: Set<ts.Node> = new Set(),
|
||||
): { links: TypeLink[]; infos: TypeInfo[] } => {
|
||||
if (!node || visited.has(node)) {
|
||||
return { links: [], infos: [] };
|
||||
}
|
||||
// This makes sure we don't end up in a loop.
|
||||
// It doesn't exclude repeated types, e.g. Array<Array<MyType>>, since we're exiting on duplicate nodes, not types.
|
||||
visited.add(node);
|
||||
|
||||
const children = node
|
||||
.getChildren()
|
||||
.flatMap(child => this.findAllTypeReferences(child, visited));
|
||||
|
||||
if (!ts.isTypeReferenceNode(node)) {
|
||||
return {
|
||||
links: children.flatMap(child => child.links),
|
||||
infos: children.flatMap(child => child.infos),
|
||||
};
|
||||
}
|
||||
|
||||
const type = this.checker.getTypeFromTypeNode(node);
|
||||
|
||||
const info = this.validateTypeDeclaration(type);
|
||||
if (!info) {
|
||||
return {
|
||||
links: children.flatMap(child => child.links),
|
||||
infos: children.flatMap(child => child.infos),
|
||||
};
|
||||
}
|
||||
const { symbol, declaration } = info;
|
||||
|
||||
const typeRefs = this.findAllTypeReferences(declaration, visited);
|
||||
|
||||
const sourceFile = declaration.getSourceFile();
|
||||
const { line } = sourceFile.getLineAndCharacterOfPosition(
|
||||
declaration.getStart(),
|
||||
);
|
||||
const file = relative(this.basePath, sourceFile.fileName);
|
||||
const typeInfo = {
|
||||
id: (symbol as any).id,
|
||||
name: symbol.name,
|
||||
text: declaration.getText().replace(/;$/, ''),
|
||||
docs: this.getNodeDocs(declaration),
|
||||
path: `${file}/${symbol.name}`,
|
||||
file,
|
||||
lineInFile: line + 1, // TS line is 0-based
|
||||
links: this.recalculateLinkOffsets(declaration, typeRefs.links),
|
||||
children: typeRefs.infos,
|
||||
};
|
||||
|
||||
const link = {
|
||||
id: typeInfo.id,
|
||||
path: typeInfo.path,
|
||||
name: typeInfo.name,
|
||||
location: [node.typeName.getStart(), node.typeName.getEnd()] as const,
|
||||
};
|
||||
|
||||
return {
|
||||
links: [link, ...children.flatMap(child => child.links)],
|
||||
infos: [typeInfo, ...children.flatMap(child => child.infos)],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a given type declaration is one that we care about.
|
||||
*/
|
||||
private validateTypeDeclaration(
|
||||
type: ts.Type | undefined,
|
||||
): undefined | { symbol: ts.Symbol; declaration: ts.Declaration } {
|
||||
if (!type) {
|
||||
return undefined;
|
||||
}
|
||||
const symbol = type.aliasSymbol || type.symbol;
|
||||
if (!symbol) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [declaration] = symbol.declarations;
|
||||
|
||||
// Don't generate standalone infos for type paramters
|
||||
if (ts.isTypeParameterDeclaration(declaration)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (declaration.getSourceFile().fileName.includes('node_modules')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { symbol, declaration };
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a tree of TypeInfos with children into a flat
|
||||
* list of TypeInfos with duplicates removed.
|
||||
*/
|
||||
private flattenTypes(descs: TypeInfo[]): TypeInfo[] {
|
||||
function getChildren(parent: TypeInfo): TypeInfo[] {
|
||||
return [
|
||||
{
|
||||
...parent,
|
||||
children: [],
|
||||
},
|
||||
...parent.children.flatMap(getChildren),
|
||||
];
|
||||
}
|
||||
|
||||
const flatDescs = descs.flatMap(getChildren);
|
||||
const seenTypes = new Set<number>();
|
||||
return flatDescs.filter(desc => {
|
||||
if (seenTypes.has(desc.id)) {
|
||||
return false;
|
||||
}
|
||||
seenTypes.add(desc.id);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate link positions within a block of code, with 0 being
|
||||
* the first character in the block.
|
||||
*/
|
||||
private recalculateLinkOffsets(
|
||||
parent: ts.Node,
|
||||
links: TypeLink[],
|
||||
): TypeLink[] {
|
||||
const parentStart = parent.getStart();
|
||||
return links.map(link => {
|
||||
const [start, end] = link.location;
|
||||
return {
|
||||
...link,
|
||||
location: [start - parentStart, end - parentStart],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 sortSelector from './sortSelector';
|
||||
import { ApiDoc, InterfaceInfo, MarkdownPrinter } from './types';
|
||||
|
||||
/**
|
||||
* The ApiDocPrinter takes a ApiDoc data structure, typically generated by an ApiDocGenerator,
|
||||
* and prints it out as a markdown doc with custom code highlighting and links.
|
||||
*/
|
||||
export default class ApiDocPrinter {
|
||||
printerFactory: () => MarkdownPrinter;
|
||||
|
||||
constructor(printerFactory: () => MarkdownPrinter) {
|
||||
this.printerFactory = printerFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print an index file with all ApiRefs and what types they implement.
|
||||
*/
|
||||
printApiIndex(apiDocs: ApiDoc[]): Buffer {
|
||||
const printer = this.printerFactory();
|
||||
|
||||
printer.header(1, 'Backstage Core Utility APIs');
|
||||
|
||||
printer.paragraph(
|
||||
'The following is a list of all Utility APIs defined by `@backstage/core`.',
|
||||
'They are available to use by plugins and components, and can be accessed ',
|
||||
'using the `useApi` hook, also provided by `@backstage/core`.',
|
||||
'For more information, see https://github.com/spotify/backstage/blob/master/docs/api/utility-apis.md.',
|
||||
);
|
||||
|
||||
for (const api of apiDocs) {
|
||||
printer.header(3, `${this.apiDisplayName(api)}`, api.id);
|
||||
|
||||
printer.paragraph(api.description);
|
||||
|
||||
const typeLinks = api.interfaceInfos.map(
|
||||
i => `[${i.name}](${printer.pageLink(i.name)})`,
|
||||
);
|
||||
printer.paragraph(
|
||||
`Implemented type${typeLinks.length > 1 ? 's' : ''}: ${typeLinks.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
|
||||
printer.paragraph(`ApiRef: ${printer.srcLink(api, api.name)}`);
|
||||
}
|
||||
|
||||
return printer.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Print documentation page for a type implemented by an ApiRef and
|
||||
*/
|
||||
printInterface(apiType: InterfaceInfo, apiDocs: ApiDoc[]): Buffer {
|
||||
const printer = this.printerFactory();
|
||||
|
||||
printer.header(1, apiType.name);
|
||||
|
||||
printer.paragraph(
|
||||
`The ${apiType.name} type is defined at ${printer.srcLink(apiType)}.`,
|
||||
);
|
||||
|
||||
const apiLinks = apiDocs
|
||||
.filter(ad => ad.interfaceInfos.some(i => i.name === apiType.name))
|
||||
.map(ad => {
|
||||
const link =
|
||||
printer.indexLink() +
|
||||
printer.headerLink(this.apiDisplayName(ad), ad.id);
|
||||
return `[${ad.name}](${link})`;
|
||||
});
|
||||
|
||||
if (apiLinks.length === 1) {
|
||||
printer.paragraph(
|
||||
`The following Utility API implements this type: ${apiLinks}`,
|
||||
);
|
||||
} else {
|
||||
printer.paragraph(`The following Utility APIs implement this type:`);
|
||||
for (const link of apiLinks) {
|
||||
printer.text(` - ${link}`);
|
||||
}
|
||||
}
|
||||
|
||||
printer.header(2, 'Members');
|
||||
|
||||
this.addInterfaceMembers(printer, apiType);
|
||||
|
||||
if (apiType.dependentTypes.length) {
|
||||
printer.header(2, 'Supporting types');
|
||||
printer.paragraph(
|
||||
'These types are part of the API declaration, but may not be unique to this API.',
|
||||
);
|
||||
|
||||
this.addInterfaceTypes(printer, apiType);
|
||||
}
|
||||
|
||||
return printer.toBuffer();
|
||||
}
|
||||
|
||||
private addInterfaceMembers(
|
||||
printer: MarkdownPrinter,
|
||||
apiType: InterfaceInfo,
|
||||
) {
|
||||
for (const member of apiType.members) {
|
||||
printer.header(
|
||||
3,
|
||||
`${member.name}${member.type === 'method' ? '()' : ''}`,
|
||||
member.path,
|
||||
);
|
||||
|
||||
for (const doc of member.docs) {
|
||||
printer.text(doc);
|
||||
}
|
||||
|
||||
printer.code(member);
|
||||
}
|
||||
}
|
||||
|
||||
private addInterfaceTypes(printer: MarkdownPrinter, apiType: InterfaceInfo) {
|
||||
for (const type of apiType.dependentTypes
|
||||
.slice()
|
||||
.sort(sortSelector(x => x.name))) {
|
||||
printer.header(3, `${type.name}`, type.path);
|
||||
|
||||
for (const doc of type.docs) {
|
||||
printer.text(doc);
|
||||
}
|
||||
printer.code(type);
|
||||
|
||||
printer.paragraph(`Defined at ${printer.srcLink(type)}.`);
|
||||
|
||||
const usageLinks = [...apiType.members, ...apiType.dependentTypes]
|
||||
.filter(member => {
|
||||
return member.links.some(link => link.id === type.id);
|
||||
})
|
||||
.map(
|
||||
({ name, path }) => `[${name}](${printer.headerLink(name, path)})`,
|
||||
);
|
||||
|
||||
if (usageLinks.length) {
|
||||
printer.paragraph(`Referenced by: ${usageLinks.join(', ')}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private apiDisplayName(api: ApiDoc): string {
|
||||
return api.name.replace(/ApiRef$/, '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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 GithubSlugger from 'github-slugger';
|
||||
import sortSelector from './sortSelector';
|
||||
import { MarkdownPrinter, TypeLink } from './types';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// TODO(Rugvip): provide through options?
|
||||
const GH_BASE_URL = 'https://github.com/spotify/backstage';
|
||||
|
||||
const COMMIT_SHA =
|
||||
process.env.COMMIT_SHA ||
|
||||
execSync('git rev-parse HEAD').toString('utf8').trim();
|
||||
|
||||
/**
|
||||
* The GithubMarkdownPrinter is a MarkdownPrinter for printing Github-flavored markdown documents.
|
||||
*/
|
||||
export default class GithubMarkdownPrinter implements MarkdownPrinter {
|
||||
private str: string = '';
|
||||
|
||||
private line(line: string = '') {
|
||||
this.str += `${line}\n`;
|
||||
}
|
||||
|
||||
text(text: string) {
|
||||
this.line(text);
|
||||
this.line();
|
||||
}
|
||||
|
||||
header(level: number, text: string) {
|
||||
this.line(`${'#'.repeat(level)} ${text}`);
|
||||
this.line();
|
||||
}
|
||||
|
||||
headerLink(heading: string): string {
|
||||
const slug = GithubSlugger.slug(heading);
|
||||
return `#${slug}`;
|
||||
}
|
||||
|
||||
indexLink() {
|
||||
return './README.md';
|
||||
}
|
||||
|
||||
pageLink(name: string) {
|
||||
return `./${name}.md`;
|
||||
}
|
||||
|
||||
srcLink(
|
||||
{ file, lineInFile }: { file: string; lineInFile: number },
|
||||
text?: string,
|
||||
): string {
|
||||
const linkText = text ?? `${file}:${lineInFile}`;
|
||||
const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`;
|
||||
return `[${linkText}](${href})`;
|
||||
}
|
||||
|
||||
paragraph(...text: string[]) {
|
||||
this.line(
|
||||
text
|
||||
.join('\n')
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.join('\n'),
|
||||
);
|
||||
this.line();
|
||||
}
|
||||
|
||||
code({ text, links = [] }: { text: string; links?: TypeLink[] }) {
|
||||
this.line('<pre>');
|
||||
this.line(this.formatWithLinks({ text, links }));
|
||||
this.line('</pre>');
|
||||
this.line();
|
||||
}
|
||||
|
||||
private escapeText: (text: string) => string = (() => {
|
||||
const escapes: { [char in string]: string } = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
};
|
||||
|
||||
return (text: string) => text.replace(/[&<>]/g, char => escapes[char]);
|
||||
})();
|
||||
|
||||
private formatWithLinks({
|
||||
text,
|
||||
links = [],
|
||||
}: {
|
||||
text: string;
|
||||
links?: TypeLink[];
|
||||
}): string {
|
||||
const sortedLinks = links.slice().sort(sortSelector(x => x.location[0]));
|
||||
|
||||
sortedLinks.reduce((lastEnd, link) => {
|
||||
if (link.location[0] <= lastEnd) {
|
||||
throw new Error(
|
||||
`overlapping link detected for ${link.path}, ${link.location}`,
|
||||
);
|
||||
}
|
||||
return link.location[1];
|
||||
}, -1);
|
||||
|
||||
const parts: Array<{ text: string; link?: boolean }> = [];
|
||||
|
||||
const endLocation = sortedLinks.reduce((prev, link) => {
|
||||
const [start, end] = link.location;
|
||||
parts.push(
|
||||
{ text: text.slice(prev, start) },
|
||||
{ text: text.slice(start, end), link: true },
|
||||
);
|
||||
return end;
|
||||
}, 0);
|
||||
|
||||
parts.push({ text: text.slice(endLocation) });
|
||||
|
||||
return parts
|
||||
.map(part => {
|
||||
if (part.link) {
|
||||
const link = this.headerLink(part.text);
|
||||
return `<a href="${link}">${this.escapeText(part.text)}</a>`;
|
||||
}
|
||||
return this.escapeText(part.text);
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
toBuffer(): Buffer {
|
||||
return Buffer.from(this.str, 'utf8');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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 sortSelector from './sortSelector';
|
||||
import { Highlighter, MarkdownPrinter, TypeLink } from './types';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// TODO(Rugvip): provide through options?
|
||||
const GH_BASE_URL = 'https://github.com/spotify/backstage';
|
||||
|
||||
const COMMIT_SHA =
|
||||
process.env.COMMIT_SHA || execSync('git rev-parse HEAD').toString('utf8');
|
||||
|
||||
/**
|
||||
* The TechdocsMarkdownPrinter is a MarkdownPrinter for building TechDocs markdown documents.
|
||||
*/
|
||||
export default class TechdocsMarkdownPrinter implements MarkdownPrinter {
|
||||
private str: string = '';
|
||||
private readonly highlighter: Highlighter;
|
||||
|
||||
constructor(highlighter: Highlighter) {
|
||||
this.highlighter = highlighter;
|
||||
|
||||
// Remove line numbers from codeblocks
|
||||
this.style('.linenodiv{ display: none }');
|
||||
}
|
||||
|
||||
private line(line: string = '') {
|
||||
this.str += `${line}\n`;
|
||||
}
|
||||
|
||||
text(text: string) {
|
||||
this.line(text);
|
||||
this.line();
|
||||
}
|
||||
|
||||
style(style: string) {
|
||||
this.line('<style>');
|
||||
this.line(style);
|
||||
this.line('</style>');
|
||||
}
|
||||
|
||||
header(level: number, text: string, id?: string) {
|
||||
this.line(`${'#'.repeat(level)} ${text}${id ? ` {#${id}}` : ''}`);
|
||||
this.line();
|
||||
}
|
||||
|
||||
headerLink(heading: string, id?: string): string {
|
||||
return `#${id ?? heading}`;
|
||||
}
|
||||
|
||||
indexLink() {
|
||||
return '../';
|
||||
}
|
||||
|
||||
pageLink(name: string) {
|
||||
return `./${name}/`;
|
||||
}
|
||||
|
||||
srcLink(
|
||||
{ file, lineInFile }: { file: string; lineInFile: number },
|
||||
text?: string,
|
||||
): string {
|
||||
const linkText = text ?? `${file}:${lineInFile}`;
|
||||
const href = `${GH_BASE_URL}/blob/${COMMIT_SHA}/${file}#L${lineInFile}`;
|
||||
return `[${linkText}](${href})`;
|
||||
}
|
||||
|
||||
paragraph(...text: string[]) {
|
||||
this.line(
|
||||
text
|
||||
.join('\n')
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.join('\n'),
|
||||
);
|
||||
this.line();
|
||||
}
|
||||
|
||||
code({ text, links = [] }: { text: string; links?: TypeLink[] }) {
|
||||
this.line('<div class="code">');
|
||||
this.line(
|
||||
'<div class="codehilite" style="background: #f0f0f0; padding: 0.225rem 0.6rem">',
|
||||
);
|
||||
this.line('<pre style="line-height: 125%">');
|
||||
this.line(this.formatWithLinks({ text, links }));
|
||||
this.line('</pre>');
|
||||
this.line('</div>');
|
||||
this.line('</div>');
|
||||
}
|
||||
|
||||
private escapeText: (text: string) => string = (() => {
|
||||
const escapes: { [char in string]: string } = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
};
|
||||
|
||||
return (text: string) => text.replace(/[&<>]/g, char => escapes[char]);
|
||||
})();
|
||||
|
||||
private formatWithLinks({
|
||||
text,
|
||||
links = [],
|
||||
}: {
|
||||
text: string;
|
||||
links?: TypeLink[];
|
||||
}): string {
|
||||
const sortedLinks = links.slice().sort(sortSelector(x => x.location[0]));
|
||||
|
||||
sortedLinks.reduce((lastEnd, link) => {
|
||||
if (link.location[0] <= lastEnd) {
|
||||
throw new Error(
|
||||
`overlapping link detected for ${link.path}, ${link.location}`,
|
||||
);
|
||||
}
|
||||
return link.location[1];
|
||||
}, -1);
|
||||
|
||||
const parts: Array<{ text: string; path?: string }> = [];
|
||||
|
||||
const endLocation = sortedLinks.reduce((prev, link) => {
|
||||
const [start, end] = link.location;
|
||||
parts.push(
|
||||
{ text: text.slice(prev, start) },
|
||||
{ text: text.slice(start, end), path: link.path },
|
||||
);
|
||||
return end;
|
||||
}, 0);
|
||||
|
||||
parts.push({ text: text.slice(endLocation) });
|
||||
|
||||
return parts
|
||||
.map(part => {
|
||||
if (part.path) {
|
||||
const link = this.headerLink(part.text, part.path);
|
||||
return `<a href="${link}">${this.escapeText(part.text)}</a>`;
|
||||
}
|
||||
return this.highlighter.highlight(this.escapeText(part.text));
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
toBuffer(): Buffer {
|
||||
return Buffer.from(this.str, 'utf8');
|
||||
}
|
||||
}
|
||||
@@ -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 ts from 'typescript';
|
||||
import TypeLocator from './TypeLocator';
|
||||
import { createMemProgram } from './testUtils';
|
||||
|
||||
describe('TypeLocator', () => {
|
||||
it('should find a default export', () => {
|
||||
const program = createMemProgram('export default class MyApi<T> {}');
|
||||
|
||||
const typeLocator = TypeLocator.fromProgram(program, '/mem');
|
||||
|
||||
const apiType = typeLocator.getExportedType('/mem/index.ts');
|
||||
expect(apiType.symbol.name).toBe('default');
|
||||
const [declaration] = apiType.symbol.declarations;
|
||||
expect(declaration.kind).toBe(ts.SyntaxKind.ClassDeclaration);
|
||||
expect((declaration as ts.ClassDeclaration).name).toBeDefined();
|
||||
expect((declaration as ts.ClassDeclaration).name!.text).toBe('MyApi');
|
||||
}, 10000);
|
||||
|
||||
it('should find api instance export', () => {
|
||||
const program = createMemProgram(
|
||||
`
|
||||
import MyApi from './type';
|
||||
|
||||
type MyApiType = {};
|
||||
|
||||
export const myApi = new MyApi<MyApiType>();
|
||||
`,
|
||||
{
|
||||
'/mem/type.ts': 'export default class MyApi<T> {}',
|
||||
},
|
||||
);
|
||||
|
||||
const typeLocator = TypeLocator.fromProgram(program, '/mem');
|
||||
|
||||
const { apiInstances } = typeLocator.findExportedInstances({
|
||||
apiInstances: typeLocator.getExportedType('/mem/type.ts'),
|
||||
});
|
||||
|
||||
expect(apiInstances.length).toBe(1);
|
||||
const [apiInstance] = apiInstances;
|
||||
expect(apiInstance.name).toBe('myApi');
|
||||
expect(apiInstance.source.fileName).toBe('/mem/index.ts');
|
||||
expect(apiInstance.args).toEqual([]);
|
||||
|
||||
expect(apiInstance.typeArgs.length).toBe(1);
|
||||
const [typeArg] = apiInstance.typeArgs;
|
||||
expect(typeArg.kind).toBe(ts.SyntaxKind.TypeReference);
|
||||
expect((typeArg as ts.TypeReferenceNode).typeName.getText()).toBe(
|
||||
'MyApiType',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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 ts from 'typescript';
|
||||
import { ExportedInstance } from './types';
|
||||
|
||||
/**
|
||||
* The TypeLocator is used to extract typescrint Type structures from a compiled program,
|
||||
* as well as finding locations where types are used in according to a matching pattern.
|
||||
*
|
||||
* This is used to e.g. find exported APIs that we should generate documentation for.
|
||||
*/
|
||||
export default class TypeLocator {
|
||||
static fromProgram(program: ts.Program, sourcePath: string) {
|
||||
return new TypeLocator(program.getTypeChecker(), program, sourcePath);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly checker: ts.TypeChecker,
|
||||
private readonly program: ts.Program,
|
||||
private readonly sourcePath: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the type of a symbol by export path and name
|
||||
*/
|
||||
getExportedType(path: string, exportedName: string = 'default'): ts.Type {
|
||||
const source = this.program.getSourceFile(path);
|
||||
if (!source) {
|
||||
throw new Error(`Source not found for path '${path}'`);
|
||||
}
|
||||
const exported = this.checker.getExportsOfModule(
|
||||
this.checker.getSymbolAtLocation(source)!,
|
||||
);
|
||||
const [symbol] = exported.filter(e => e.name === exportedName);
|
||||
if (!symbol) {
|
||||
throw new Error(`No export '${exportedName}' found in ${path}`);
|
||||
}
|
||||
const type = this.checker.getTypeOfSymbolAtLocation(symbol, source);
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find exported instances and return values from calls using the types
|
||||
* provided in the lookup table.
|
||||
*/
|
||||
findExportedInstances<T extends string>(
|
||||
typeLookupTable: { [key in T]: ts.Type },
|
||||
): { [key in T]: ExportedInstance[] } {
|
||||
const docMap = new Map<ts.Type, ExportedInstance[]>();
|
||||
for (const type of Object.values<ts.Type>(typeLookupTable)) {
|
||||
docMap.set(type, []);
|
||||
}
|
||||
|
||||
this.program.getSourceFiles().forEach(source => {
|
||||
const inRoot = source.fileName.startsWith(this.sourcePath);
|
||||
if (!inRoot) {
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(source, node => {
|
||||
const decl = this.getExportedConstructorDeclaration(node);
|
||||
if (!decl || !docMap.has(decl.constructorType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
docMap.get(decl.constructorType)!.push({
|
||||
node,
|
||||
name: decl.name,
|
||||
source,
|
||||
args: Array.from(decl.initializer.arguments || []),
|
||||
typeArgs: Array.from(decl.initializer.typeArguments || []),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const result: { [key in T]?: ExportedInstance[] } = {};
|
||||
|
||||
for (const key in typeLookupTable) {
|
||||
if (typeLookupTable.hasOwnProperty(key)) {
|
||||
const type = typeLookupTable[key];
|
||||
result[key] = docMap.get(type)!;
|
||||
}
|
||||
}
|
||||
|
||||
return result as { [key in T]: ExportedInstance[] };
|
||||
}
|
||||
|
||||
private getExportedConstructorDeclaration(
|
||||
node: ts.Node,
|
||||
):
|
||||
| {
|
||||
constructorType: ts.Type;
|
||||
initializer: ts.CallExpression | ts.NewExpression;
|
||||
name: string;
|
||||
}
|
||||
| undefined {
|
||||
if (!ts.isVariableStatement(node)) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!node.modifiers ||
|
||||
!node.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const { declarations } = node.declarationList;
|
||||
if (declarations.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [declaration] = declarations;
|
||||
const { initializer, name } = declaration;
|
||||
|
||||
if (!initializer || !name) {
|
||||
return undefined;
|
||||
}
|
||||
if (!ts.isCallOrNewExpression(initializer)) {
|
||||
return undefined;
|
||||
}
|
||||
if (!ts.isIdentifier(name)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const constructorType = this.checker.getTypeAtLocation(
|
||||
initializer.expression,
|
||||
);
|
||||
return { constructorType, initializer, name: name.text };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 { Highlighter } from './types';
|
||||
|
||||
// Simple syntax highlighter that mimics hilite
|
||||
export default class TypescriptHighlighter implements Highlighter {
|
||||
private static readonly basicTypes = [
|
||||
'boolean',
|
||||
'number',
|
||||
'string',
|
||||
'Array',
|
||||
'object',
|
||||
'Record',
|
||||
'Set',
|
||||
'Map',
|
||||
'true',
|
||||
'false',
|
||||
'null',
|
||||
'undefined',
|
||||
'void',
|
||||
'Promise',
|
||||
'any',
|
||||
'[0-9\\.]+',
|
||||
];
|
||||
|
||||
// List of highlightings to apply, each with a match regex and the style that should be applied
|
||||
private static readonly highlighters = [
|
||||
[/(\/\*\*?(?:.|\n)+?\*\/)/g, 'color: #60a0b0; font-style: italic'], // block comment
|
||||
[/(\/\/.*)/gm, 'color: #60a0b0; font-style: italic'], // line comment
|
||||
[/('[^']*?'|"[^"]*?")/g, 'color: #4070a0'], // string literals
|
||||
[
|
||||
new RegExp(
|
||||
`\\b(${TypescriptHighlighter.basicTypes.join('|')})\\b(?!:|,)`,
|
||||
'g',
|
||||
),
|
||||
'color: #902000',
|
||||
], // basic types
|
||||
[
|
||||
/^((?:export\s)?(?:type\s)?(?:interface\s)?)/g,
|
||||
'color: #007020; font-weight: bold',
|
||||
], // keywords
|
||||
] as readonly [RegExp, string][];
|
||||
|
||||
highlight(fullText: string): string {
|
||||
// Each part is either plain text that can be highlighted or text that is already highlighted
|
||||
type HighlightPart = { text: string; highlighted?: boolean };
|
||||
|
||||
const painter = (regex: RegExp, style: string) => (part: HighlightPart) => {
|
||||
if (part.highlighted) {
|
||||
return [part];
|
||||
}
|
||||
// Apply highlighting to all matches of the regex by splitting into parts
|
||||
return part.text.split(regex).map((text, index) => {
|
||||
// Odd parts are the ones that matched the regex and should be highlighted.
|
||||
if (index % 2 === 1) {
|
||||
return {
|
||||
text: `<span style="${style}">${text}</span>`,
|
||||
highlighted: true,
|
||||
};
|
||||
}
|
||||
return { text };
|
||||
});
|
||||
};
|
||||
|
||||
// Order here is important, e.g. comments must be first to avoid string literals inside comments being highlighted
|
||||
return TypescriptHighlighter.highlighters
|
||||
.reduce((parts, highlighter) => parts.flatMap(painter(...highlighter)), [
|
||||
{ text: fullText },
|
||||
])
|
||||
.map(({ text }) => text)
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 sortSelector from './sortSelector';
|
||||
|
||||
describe('sortSelector', () => {
|
||||
it('should stable sort', () => {
|
||||
const arr = [
|
||||
[3, 1],
|
||||
[1, 2],
|
||||
[1, 1],
|
||||
[1, 3],
|
||||
[2, 1],
|
||||
];
|
||||
|
||||
const sortedByFirst = arr.slice().sort(sortSelector(([first]) => first));
|
||||
expect(sortedByFirst).toEqual([
|
||||
[1, 2],
|
||||
[1, 1],
|
||||
[1, 3],
|
||||
[2, 1],
|
||||
[3, 1],
|
||||
]);
|
||||
|
||||
const sortedBySecond = arr
|
||||
.slice()
|
||||
.sort(sortSelector(([, second]) => second));
|
||||
expect(sortedBySecond).toEqual([
|
||||
[3, 1],
|
||||
[1, 1],
|
||||
[2, 1],
|
||||
[1, 2],
|
||||
[1, 3],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The sortSelector is a utility that makes a sort function by selecting the value to sort on with a lambda.
|
||||
*/
|
||||
export default function sortSelector<T>(
|
||||
selector: (x: T) => any,
|
||||
): (a: T, b: T) => -1 | 1 | 0 {
|
||||
return (a: T, b: T) => {
|
||||
const aV = selector(a);
|
||||
const bV = selector(b);
|
||||
if (aV < bV) {
|
||||
return -1;
|
||||
} else if (aV > bV) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 ts from 'typescript';
|
||||
|
||||
export function createMemProgram(
|
||||
indexSource: string,
|
||||
otherSourceFiles: { [fileName in string]: string } = {},
|
||||
): ts.Program {
|
||||
const rootDir = '/mem';
|
||||
|
||||
const options = { noEmit: true };
|
||||
const baseHost = ts.createCompilerHost(options);
|
||||
const files: { [fileName in string]: string } = {
|
||||
[`${rootDir}/index.ts`]: indexSource,
|
||||
...otherSourceFiles,
|
||||
};
|
||||
|
||||
// Custom compiler hosts that reads from a map of in-memory files, but
|
||||
// falls back to reading from disc for ts libs etc.
|
||||
const compilerHost: ts.CompilerHost = {
|
||||
...baseHost,
|
||||
readFile(fileName): string | undefined {
|
||||
if (fileName in files) {
|
||||
return files[fileName];
|
||||
}
|
||||
return baseHost.readFile(fileName);
|
||||
},
|
||||
getCurrentDirectory(): string {
|
||||
return rootDir;
|
||||
},
|
||||
directoryExists(dir) {
|
||||
if (dir === rootDir) {
|
||||
return true;
|
||||
}
|
||||
if (baseHost.directoryExists) {
|
||||
return baseHost.directoryExists(dir);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
fileExists(fileName: string): boolean {
|
||||
return !!files[fileName] || baseHost.fileExists(fileName);
|
||||
},
|
||||
getSourceFile(fileName, ...rest): ts.SourceFile | undefined {
|
||||
const file = files[fileName];
|
||||
if (file) {
|
||||
return ts.createSourceFile(fileName, file, ts.ScriptTarget.ES2017);
|
||||
}
|
||||
return baseHost.getSourceFile(fileName, ...rest);
|
||||
},
|
||||
};
|
||||
|
||||
return ts.createProgram(['/mem/index.ts'], options, compilerHost);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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 ts from 'typescript';
|
||||
|
||||
/**
|
||||
* A TypeLink is a link to a different type.
|
||||
*/
|
||||
export type TypeLink = {
|
||||
// The ID of the linked type
|
||||
id: number;
|
||||
// The path to the type from the project root
|
||||
path: string;
|
||||
// The name of the type, to display
|
||||
name: string;
|
||||
// The location of the type name as it appears in it's parent text.
|
||||
location: readonly [number, number];
|
||||
};
|
||||
|
||||
/**
|
||||
* TypeInfo describes a Typescript Type.
|
||||
*/
|
||||
export type TypeInfo = {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
file: string;
|
||||
lineInFile: number;
|
||||
text: string;
|
||||
docs: string[];
|
||||
links: TypeLink[];
|
||||
children: TypeInfo[];
|
||||
};
|
||||
|
||||
/**
|
||||
* FieldInfo describes a property or method in a documented API.
|
||||
*/
|
||||
export type FieldInfo = {
|
||||
type: 'prop' | 'method';
|
||||
name: string;
|
||||
path: string;
|
||||
text: string;
|
||||
docs: string[];
|
||||
links: TypeLink[];
|
||||
};
|
||||
|
||||
/**
|
||||
* InterfaceInfo describes the type of a documented API.
|
||||
*/
|
||||
export type InterfaceInfo = {
|
||||
name: string;
|
||||
docs: string[];
|
||||
file: string;
|
||||
lineInFile: number;
|
||||
members: Array<FieldInfo>;
|
||||
dependentTypes: TypeInfo[];
|
||||
};
|
||||
|
||||
/**
|
||||
* ApiDoc describes a documented API.
|
||||
*/
|
||||
export type ApiDoc = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
file: string;
|
||||
lineInFile: number;
|
||||
interfaceInfos: InterfaceInfo[];
|
||||
};
|
||||
|
||||
/**
|
||||
* ExportedInstance describes an expression matching `export {name} = new {Contructor}<{typeArgs}...>({args}...)`
|
||||
*/
|
||||
export type ExportedInstance = {
|
||||
node: ts.Node;
|
||||
name: string;
|
||||
source: ts.SourceFile;
|
||||
args: Array<ts.Expression>;
|
||||
typeArgs: Array<ts.TypeNode>;
|
||||
};
|
||||
|
||||
export type Highlighter = {
|
||||
highlight(test: string): string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Markdown printer is an abstraction for printing markdown documents of different flavors.
|
||||
*/
|
||||
export type MarkdownPrinter = {
|
||||
text(text: string): void;
|
||||
header(level: number, text: string, id?: string): void;
|
||||
paragraph(...text: string[]): void;
|
||||
code(options: { text: string; links?: TypeLink[] }): void;
|
||||
|
||||
headerLink(header: string, id?: string): string;
|
||||
/** Link from pages to index */
|
||||
indexLink(): string;
|
||||
/** Link from index to pages */
|
||||
pageLink(name: string): string;
|
||||
srcLink(
|
||||
{ file, lineInFile }: { file: string; lineInFile: number },
|
||||
text?: string,
|
||||
): string;
|
||||
|
||||
toBuffer(): Buffer;
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 * as ts from 'typescript';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath, join as joinPath } from 'path';
|
||||
import ApiDocGenerator from './docgen/ApiDocGenerator';
|
||||
import sortSelector from './docgen/sortSelector';
|
||||
import TypeLocator from './docgen/TypeLocator';
|
||||
import ApiDocPrinter from './docgen/ApiDocsPrinter';
|
||||
import TypescriptHighlighter from './docgen/TypescriptHighlighter';
|
||||
import GitHubMarkdownPrinter from './docgen/GitHubMarkdownPrinter';
|
||||
import TechdocsMarkdownPrinter from './docgen/TechdocsMarkdownPrinter';
|
||||
|
||||
const FORMATS = ['github', 'techdocs'] as const;
|
||||
|
||||
export async function generate(
|
||||
targetPath: string,
|
||||
format: typeof FORMATS[number],
|
||||
) {
|
||||
if (!FORMATS.includes(format)) {
|
||||
throw new TypeError(
|
||||
`Invalid format, '${format}', must be one of ${FORMATS.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const rootDir = resolvePath(__dirname, '../../..');
|
||||
const srcDir = resolvePath(rootDir, 'packages', 'core-api', 'src');
|
||||
const targetDir = resolvePath(targetPath);
|
||||
|
||||
const options = await fs.readJson(resolvePath('../cli/config/tsconfig.json'));
|
||||
|
||||
delete options.moduleResolution;
|
||||
options.noEmit = true;
|
||||
|
||||
const program = ts.createProgram([resolvePath(srcDir, 'index.ts')], options);
|
||||
|
||||
const typeLocator = TypeLocator.fromProgram(program, srcDir);
|
||||
|
||||
const { apis } = typeLocator.findExportedInstances({
|
||||
apis: typeLocator.getExportedType(
|
||||
resolvePath(srcDir, 'index.ts'),
|
||||
'createApiRef',
|
||||
),
|
||||
});
|
||||
|
||||
const apiDocGenerator = ApiDocGenerator.fromProgram(program, rootDir);
|
||||
const apiDocs = apis
|
||||
.map(api => {
|
||||
try {
|
||||
return apiDocGenerator.toDoc(api);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Doc generation failed for API in ${api.source.fileName}, ${error.stack}`,
|
||||
);
|
||||
}
|
||||
})
|
||||
.sort(sortSelector(x => x.name));
|
||||
|
||||
const apiTypes = Object.values(
|
||||
Object.fromEntries(
|
||||
apiDocs.flatMap(d => d.interfaceInfos).map(i => [i.name, i]),
|
||||
),
|
||||
).sort(sortSelector(i => i.name));
|
||||
|
||||
if (format === 'techdocs') {
|
||||
const docsDir = resolvePath(targetDir, 'docs');
|
||||
await fs.ensureDir(docsDir);
|
||||
|
||||
const apiDocPrinter = new ApiDocPrinter(
|
||||
() => new TechdocsMarkdownPrinter(new TypescriptHighlighter()),
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
joinPath(docsDir, 'README.md'),
|
||||
apiDocPrinter.printApiIndex(apiDocs),
|
||||
);
|
||||
|
||||
for (const apiType of Object.values(apiTypes)) {
|
||||
const data = apiDocPrinter.printInterface(apiType, apiDocs);
|
||||
|
||||
await fs.writeFile(joinPath(docsDir, `${apiType.name}.md`), data);
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
resolvePath(targetDir, 'mkdocs.yml'),
|
||||
[
|
||||
'site_name: Backstage Core Utility API References',
|
||||
'nav:',
|
||||
` - API Index: 'README.md'`,
|
||||
...apiTypes.map(({ name }) => ` - ${name}: '${name}.md'`),
|
||||
'plugins:',
|
||||
' - techdocs-core',
|
||||
].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
} else {
|
||||
await fs.ensureDir(targetDir);
|
||||
|
||||
const apiDocPrinter = new ApiDocPrinter(() => new GitHubMarkdownPrinter());
|
||||
|
||||
await fs.writeFile(
|
||||
joinPath(targetDir, 'README.md'),
|
||||
apiDocPrinter.printApiIndex(apiDocs),
|
||||
);
|
||||
|
||||
for (const apiType of Object.values(apiTypes)) {
|
||||
const data = apiDocPrinter.printInterface(apiType, apiDocs);
|
||||
|
||||
await fs.writeFile(joinPath(targetDir, `${apiType.name}.md`), data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 program from 'commander';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import { generate } from './generate';
|
||||
|
||||
const main = (argv: string[]) => {
|
||||
const pkgJson = fs.readJsonSync(resolvePath(__dirname, '../package.json'));
|
||||
program.name('docgen').version(pkgJson.version);
|
||||
|
||||
program
|
||||
.command('generate')
|
||||
.description(
|
||||
'Generate documentation for the declarations in the core-api package',
|
||||
)
|
||||
.option('--output <output>', 'Output directory [./dist]')
|
||||
.option(
|
||||
'--format <format>',
|
||||
'Output format, either techdocs or github [techdocs]',
|
||||
)
|
||||
.action(async cmd => {
|
||||
await generate(cmd.output ?? './dist', cmd.format ?? 'techdocs');
|
||||
});
|
||||
|
||||
program.on('command:*', () => {
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.red(`Invalid command: ${chalk.cyan(program.args.join(' '))}`),
|
||||
);
|
||||
console.log(chalk.red('See --help for a list of available commands.'));
|
||||
console.log();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp(chalk.yellow);
|
||||
}
|
||||
|
||||
program.parse(argv);
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', rejection => {
|
||||
console.error(String(rejection));
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
main(process.argv);
|
||||
@@ -18,10 +18,14 @@ import {
|
||||
OAuthRequestManager,
|
||||
OktaAuth,
|
||||
oktaAuthApiRef,
|
||||
configApiRef,
|
||||
ConfigReader,
|
||||
} from '@backstage/core';
|
||||
|
||||
const builder = ApiRegistry.builder();
|
||||
|
||||
builder.add(configApiRef, ConfigReader.fromConfigs([]));
|
||||
|
||||
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
|
||||
|
||||
builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder()));
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"react-dom": "^16.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"zen-observable": "^0.8.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.2.2",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^12.0.0"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -33,7 +33,7 @@ export class EnvironmentHandler implements AuthProviderRouteHandlers {
|
||||
): AuthProviderRouteHandlers | undefined {
|
||||
const env = req.query.env?.toString();
|
||||
|
||||
if (this.providers.hasOwnProperty(env)) {
|
||||
if (env && this.providers.hasOwnProperty(env)) {
|
||||
return this.providers[env];
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ import { createOAuth2Provider } from './oauth2';
|
||||
import { createOktaProvider } from './okta';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { AuthProviderConfig, AuthProviderFactory } from './types';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../lib/EnvironmentHandler';
|
||||
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
google: createGoogleProvider,
|
||||
@@ -37,7 +42,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
export const createAuthProviderRouter = (
|
||||
providerId: string,
|
||||
globalConfig: AuthProviderConfig,
|
||||
providerConfig: any, // TODO: make this a config reader object of sorts
|
||||
providerConfig: Config,
|
||||
logger: Logger,
|
||||
issuer: TokenIssuer,
|
||||
) => {
|
||||
@@ -46,17 +51,28 @@ export const createAuthProviderRouter = (
|
||||
throw Error(`No auth provider available for '${providerId}'`);
|
||||
}
|
||||
|
||||
const provider = factory(globalConfig, providerConfig, logger, issuer);
|
||||
|
||||
const router = Router();
|
||||
router.get('/start', provider.start.bind(provider));
|
||||
router.get('/handler/frame', provider.frameHandler.bind(provider));
|
||||
router.post('/handler/frame', provider.frameHandler.bind(provider));
|
||||
if (provider.logout) {
|
||||
router.post('/logout', provider.logout.bind(provider));
|
||||
const envs = providerConfig.keys();
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
|
||||
for (const env of envs) {
|
||||
const envConfig = providerConfig.getConfig(env);
|
||||
const provider = factory(globalConfig, env, envConfig, logger, issuer);
|
||||
if (provider) {
|
||||
envProviders[env] = provider;
|
||||
}
|
||||
}
|
||||
if (provider.refresh) {
|
||||
router.get('/refresh', provider.refresh.bind(provider));
|
||||
|
||||
const handler = new EnvironmentHandler(providerId, envProviders);
|
||||
|
||||
router.get('/start', handler.start.bind(handler));
|
||||
router.get('/handler/frame', handler.frameHandler.bind(handler));
|
||||
router.post('/handler/frame', handler.frameHandler.bind(handler));
|
||||
if (handler.logout) {
|
||||
router.post('/logout', handler.logout.bind(handler));
|
||||
}
|
||||
if (handler.refresh) {
|
||||
router.get('/refresh', handler.refresh.bind(handler));
|
||||
}
|
||||
|
||||
return router;
|
||||
|
||||
@@ -25,20 +25,15 @@ import {
|
||||
OAuthProviderHandlers,
|
||||
AuthProviderConfig,
|
||||
RedirectInfo,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import passport from 'passport';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly _strategy: GithubStrategy;
|
||||
@@ -131,44 +126,43 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createGithubProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'github';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const opts = {
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
};
|
||||
const opts = {
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), {
|
||||
disableRefresh: true,
|
||||
persistScopes: true,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new GithubAuthProvider(opts), {
|
||||
disableRefresh: true,
|
||||
persistScopes: true,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,20 +25,15 @@ import {
|
||||
OAuthProviderHandlers,
|
||||
AuthProviderConfig,
|
||||
RedirectInfo,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import passport from 'passport';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export class GitlabAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly _strategy: GitlabStrategy;
|
||||
@@ -138,49 +133,45 @@ export class GitlabAuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createGitlabProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'gitlab';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const audience = envConfig.getString('audience');
|
||||
const baseURL = audience || 'https://gitlab.com';
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const {
|
||||
secure,
|
||||
appOrigin,
|
||||
clientId,
|
||||
clientSecret,
|
||||
audience,
|
||||
} = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const opts = {
|
||||
clientID: clientId,
|
||||
clientSecret: clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
baseURL: audience,
|
||||
};
|
||||
const opts = {
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
baseURL,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable',
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new GitlabAuthProvider(opts), {
|
||||
disableRefresh: true,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new GitlabAuthProvider(opts), {
|
||||
disableRefresh: true,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,20 +27,15 @@ import {
|
||||
OAuthProviderHandlers,
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import passport from 'passport';
|
||||
import {
|
||||
EnvironmentHandler,
|
||||
EnvironmentHandlers,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -150,43 +145,42 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createGoogleProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'google';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const opts = {
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
};
|
||||
const opts = {
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
|
||||
if (!opts.clientID || !opts.clientSecret) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new GoogleAuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new GoogleAuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@ import passport from 'passport';
|
||||
import { Strategy as OAuth2Strategy } from 'passport-oauth2';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import {
|
||||
EnvironmentHandler,
|
||||
EnvironmentHandlers,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { OAuthProvider } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
executeFetchUserProfileStrategy,
|
||||
@@ -33,14 +29,13 @@ import {
|
||||
} from '../../lib/PassportStrategyHelper';
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
EnvironmentProviderConfig,
|
||||
GenericOAuth2ProviderConfig,
|
||||
GenericOAuth2ProviderOptions,
|
||||
OAuthProviderHandlers,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
RedirectInfo,
|
||||
} from '../types';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -148,51 +143,51 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createOAuth2Provider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'oauth2';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
const authorizationURL = envConfig.getString('authorizationURL');
|
||||
const tokenURL = envConfig.getString('tokenURL');
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as GenericOAuth2ProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const opts = {
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
authorizationURL: config.authorizationURL,
|
||||
tokenURL: config.tokenURL,
|
||||
};
|
||||
const opts = {
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
authorizationURL,
|
||||
tokenURL,
|
||||
};
|
||||
|
||||
if (
|
||||
!opts.clientID ||
|
||||
!opts.clientSecret ||
|
||||
!opts.authorizationURL ||
|
||||
!opts.tokenURL
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable',
|
||||
if (
|
||||
!opts.clientID ||
|
||||
!opts.clientSecret ||
|
||||
!opts.authorizationURL ||
|
||||
!opts.tokenURL
|
||||
) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new OAuth2AuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new OAuth2AuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,19 +28,14 @@ import {
|
||||
OAuthProviderHandlers,
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
EnvironmentProviderConfig,
|
||||
OAuthProviderOptions,
|
||||
OAuthProviderConfig,
|
||||
OAuthResponse,
|
||||
PassportDoneCallback,
|
||||
} from '../types';
|
||||
import {
|
||||
EnvironmentHandler,
|
||||
EnvironmentHandlers,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { StateStore } from 'passport-oauth2';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type PrivateInfo = {
|
||||
refreshToken: string;
|
||||
@@ -170,45 +165,44 @@ export class OktaAuthProvider implements OAuthProviderHandlers {
|
||||
|
||||
export function createOktaProvider(
|
||||
{ baseUrl }: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const providerId = 'okta';
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const secure = envConfig.getBoolean('secure');
|
||||
const appOrigin = envConfig.getString('appOrigin');
|
||||
const clientID = envConfig.getString('clientId');
|
||||
const clientSecret = envConfig.getString('clientSecret');
|
||||
const audience = envConfig.getString('audience');
|
||||
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as OAuthProviderConfig;
|
||||
const { secure, appOrigin } = config;
|
||||
const opts = {
|
||||
audience: config.audience,
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
|
||||
};
|
||||
const opts = {
|
||||
audience,
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
};
|
||||
|
||||
if (!opts.clientID || !opts.clientSecret || !opts.audience) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars',
|
||||
);
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable',
|
||||
if (!opts.clientID || !opts.clientSecret || !opts.audience) {
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
throw new Error(
|
||||
'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new OAuthProvider(new OktaAuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
logger.warn(
|
||||
'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new EnvironmentHandler(providerId, envProviders);
|
||||
return new OAuthProvider(new OktaAuthProvider(opts), {
|
||||
disableRefresh: false,
|
||||
providerId,
|
||||
secure,
|
||||
baseUrl,
|
||||
appOrigin,
|
||||
tokenIssuer,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,18 +27,13 @@ import {
|
||||
import {
|
||||
AuthProviderConfig,
|
||||
AuthProviderRouteHandlers,
|
||||
EnvironmentProviderConfig,
|
||||
SAMLProviderConfig,
|
||||
PassportDoneCallback,
|
||||
ProfileInfo,
|
||||
} from '../types';
|
||||
import { postMessageResponse } from '../../lib/OAuthProvider';
|
||||
import {
|
||||
EnvironmentHandlers,
|
||||
EnvironmentHandler,
|
||||
} from '../../lib/EnvironmentHandler';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type SamlInfo = {
|
||||
userId: string;
|
||||
@@ -122,30 +117,25 @@ type SAMLProviderOptions = {
|
||||
|
||||
export function createSamlProvider(
|
||||
_authProviderConfig: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
_env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
tokenIssuer: TokenIssuer,
|
||||
) {
|
||||
const envProviders: EnvironmentHandlers = {};
|
||||
const entryPoint = envConfig.getString('entryPoint');
|
||||
const issuer = envConfig.getString('issuer');
|
||||
const opts = {
|
||||
entryPoint,
|
||||
issuer,
|
||||
path: '/auth/saml/handler/frame',
|
||||
tokenIssuer,
|
||||
};
|
||||
|
||||
for (const [env, envConfig] of Object.entries(providerConfig)) {
|
||||
const config = (envConfig as unknown) as SAMLProviderConfig;
|
||||
const opts = {
|
||||
entryPoint: config.entryPoint,
|
||||
issuer: config.issuer,
|
||||
path: '/auth/saml/handler/frame',
|
||||
tokenIssuer,
|
||||
};
|
||||
|
||||
if (!opts.entryPoint || !opts.issuer) {
|
||||
logger.warn(
|
||||
'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
envProviders[env] = new SamlAuthProvider(opts);
|
||||
if (!opts.entryPoint || !opts.issuer) {
|
||||
logger.warn(
|
||||
'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new EnvironmentHandler('saml', envProviders);
|
||||
return new SamlAuthProvider(opts);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
import express from 'express';
|
||||
import { Logger } from 'winston';
|
||||
import { TokenIssuer } from '../identity';
|
||||
import { Config } from '@backstage/config';
|
||||
import { OAuthProvider } from '../lib/OAuthProvider';
|
||||
import { SamlAuthProvider } from './saml/provider';
|
||||
|
||||
export type OAuthProviderOptions = {
|
||||
/**
|
||||
@@ -204,10 +207,11 @@ export interface AuthProviderRouteHandlers {
|
||||
|
||||
export type AuthProviderFactory = (
|
||||
globalConfig: AuthProviderConfig,
|
||||
providerConfig: EnvironmentProviderConfig,
|
||||
env: string,
|
||||
envConfig: Config,
|
||||
logger: Logger,
|
||||
issuer: TokenIssuer,
|
||||
) => AuthProviderRouteHandlers;
|
||||
) => OAuthProvider | SamlAuthProvider | undefined;
|
||||
|
||||
export type AuthResponse<ProviderInfo> = {
|
||||
providerInfo: ProviderInfo;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user