plugins/graphiql: add GitHub endpoint with automatic scope detection

This commit is contained in:
Patrik Oldsberg
2020-06-23 21:18:28 +02:00
parent 45cdbdd4b7
commit cd7c1efb13
2 changed files with 108 additions and 2 deletions
+12 -2
View File
@@ -15,15 +15,25 @@
*/
import { createDevApp } from '@backstage/dev-utils';
import { githubAuthApiRef, errorApiRef } from '@backstage/core';
import { plugin, GraphQLEndpoints, graphQlBrowseApiRef } from '../src';
createDevApp()
.registerPlugin(plugin)
.registerApiFactory({
implements: graphQlBrowseApiRef,
deps: {},
factory() {
deps: {
errorApi: errorApiRef,
githubAuthApi: githubAuthApiRef,
},
factory({ errorApi, githubAuthApi }) {
return GraphQLEndpoints.from([
GraphQLEndpoints.github({
id: 'github',
title: 'GitHub',
errorApi,
githubAuthApi,
}),
GraphQLEndpoints.create({
id: 'gitlab',
title: 'GitLab',
@@ -15,6 +15,7 @@
*/
import { GraphQLBrowseApi, GraphQLEndpoint } from './types';
import { ErrorApi, OAuthApi } from '@backstage/core';
// Helper for generic http endpoints
export type EndpointConfig = {
@@ -28,6 +29,23 @@ export type EndpointConfig = {
headers?: { [name in string]: string };
};
export type GithubEndpointConfig = {
id: string;
title: string;
/**
* Github GraphQL API url, defaults to https://api.github.com/graphql
*/
url?: string;
/**
* Errors will be posted to the ErrorApi if it is provided.
*/
errorApi?: ErrorApi;
/**
* GitHub Auth API used to authenticate requests.
*/
githubAuthApi: OAuthApi;
};
export class GraphQLEndpoints implements GraphQLBrowseApi {
// Create a support
static create(config: EndpointConfig): GraphQLEndpoint {
@@ -51,6 +69,84 @@ export class GraphQLEndpoints implements GraphQLBrowseApi {
};
}
/**
* Creates a GitHub GraphQL endpoint that uses the GithubAuth API to authenticate requests.
*
* If a request requires more permissions than is granted by the existing session,
* the fetcher will automatically ask for the additional scopes that are required.
*/
static github(config: GithubEndpointConfig): GraphQLEndpoint {
const {
id,
title,
url = 'https://api.github.com/graphql',
errorApi,
githubAuthApi,
} = config;
type ResponseBody = {
errors?: Array<{ type: string; message: string }>;
};
return {
id,
title,
fetcher: async (params: any) => {
let retried = false;
const doRequest = async (): Promise<any> => {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${await githubAuthApi.getAccessToken()}`,
},
body: JSON.stringify(params),
});
if (!res.ok) {
throw new Error(
`Request failed with status ${res.status} ${res.statusText}`,
);
}
const data = (await res.json()) as ResponseBody;
if (!data.errors || retried) {
return data;
}
retried = true;
const missingScopes = data.errors
.filter(({ type }) => type === 'INSUFFICIENT_SCOPES')
.flatMap(({ message }) => {
const scopesMatch = message.match(
/one of the following scopes: (\[.*?\])/,
);
if (!scopesMatch) {
return [];
}
try {
const scopes = JSON.parse(scopesMatch[1].replace(/'/g, '"'));
return scopes;
} catch (error) {
if (errorApi) {
errorApi.post(
new Error(
`Failed to parse scope string "${scopesMatch[1]}", ${error}`,
),
);
}
return [];
}
});
await githubAuthApi.getAccessToken(missingScopes);
return doRequest();
};
return await doRequest();
},
};
}
static from(endpoints: GraphQLEndpoint[]) {
return new GraphQLEndpoints(endpoints);
}