Skip to content

Commit

Permalink
Merge branch 'main' into tm/upgrade-graphql-modules
Browse files Browse the repository at this point in the history
  • Loading branch information
wKich authored Feb 7, 2024
2 parents 84f2970 + 47dce29 commit 0e475db
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 33 deletions.
12 changes: 12 additions & 0 deletions plugins/graphql-backend-module-catalog/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# @frontside/backstage-plugin-graphql-backend-module-catalog

## 0.2.3

### Patch Changes

- e3d21a3: Corrects the request type used for extracting auth token for catalog requests.

## 0.2.2

### Patch Changes

- 2124df0: Pass Backstage auth token to Catalog client requests

## 0.2.1

### Patch Changes
Expand Down
3 changes: 2 additions & 1 deletion plugins/graphql-backend-module-catalog/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@frontside/backstage-plugin-graphql-backend-module-catalog",
"description": "Backstage GraphQL backend module that adds catalog schema",
"version": "0.2.1",
"version": "0.2.3",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
Expand Down Expand Up @@ -44,6 +44,7 @@
"@backstage/backend-plugin-api": "^0.6.7",
"@backstage/catalog-client": "^1.4.6",
"@backstage/catalog-model": "^1.4.3",
"@backstage/plugin-auth-node": "^0.4.1",
"@backstage/plugin-catalog-node": "^1.5.0",
"@frontside/backstage-plugin-graphql-backend": "^0.1.4",
"@frontside/hydraphql": "^0.1.1",
Expand Down
9 changes: 7 additions & 2 deletions plugins/graphql-backend-module-catalog/src/entitiesLoadFn.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import type { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { NodeQuery } from '@frontside/hydraphql';
import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
import { GraphQLContext, NodeQuery } from '@frontside/hydraphql';
import { GraphQLError } from 'graphql';
import type { Request } from 'node-fetch';
import { CATALOG_SOURCE } from './constants';

export const createCatalogLoader = (catalog: CatalogApi) => ({
[CATALOG_SOURCE]: async (
queries: readonly (NodeQuery | undefined)[],
context: GraphQLContext & { request?: Request }
): Promise<Array<Entity | GraphQLError>> => {
// TODO: Support fields
const request = context.request;
const token = getBearerTokenFromAuthorizationHeader(request?.headers.get('authorization'));
const entityRefs = queries.reduce(
(refs, { ref } = {}, index) => (ref ? refs.set(index, ref) : refs),
new Map<number, string>(),
);
const refEntries = [...entityRefs.entries()];
const result = await catalog.getEntitiesByRefs({
entityRefs: refEntries.map(([, ref]) => ref),
});
}, { token });
const entities: (Entity | GraphQLError)[] = Array.from({
length: queries.length,
});
Expand Down
2 changes: 1 addition & 1 deletion plugins/graphql-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export const myModule = createModule({
```ts
// packages/backend/src/modules/graphqlMyModule.ts
import { createBackendModule } from "@backstage/backend-plugin-api";
import { graphqlModulesExtensionPoint } from "@backstage/plugin-graphql-backend-node";
import { graphqlModulesExtensionPoint } from "@frontside/backstage-plugin-graphql-backend-node";
import { MyModule } from "../modules/my-module/my-module";

export const graphqlModuleMyModule = createBackendModule({
Expand Down
6 changes: 6 additions & 0 deletions plugins/humanitec-backend/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @frontside/backstage-plugin-humanitec-backend

## 0.3.15

### Patch Changes

- b9e57e3: Continue polling when an error is returned.

## 0.3.14

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion plugins/humanitec-backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@frontside/backstage-plugin-humanitec-backend",
"version": "0.3.14",
"version": "0.3.15",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
Expand Down
126 changes: 126 additions & 0 deletions plugins/humanitec-backend/src/service/app-info-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { setTimeout } from 'node:timers/promises';
import * as common from '@frontside/backstage-plugin-humanitec-common';

import { AppInfoService } from './app-info-service';

const fetchInterval = 50;

let returnError = false;
const fakeAppInfo = { fake: 'res' }
const fakeError = new Error('fake error');

jest.mock('@frontside/backstage-plugin-humanitec-common', () => ({
createHumanitecClient: jest.fn(),
fetchAppInfo: jest.fn(async () => {
if (returnError) {
throw fakeError;
}

return fakeAppInfo;
}),
}))

describe('AppInfoService', () => {
afterEach(() => {
jest.clearAllMocks();
returnError = false;
});

it('single subscriber', async () => {
const service = new AppInfoService('token', fetchInterval);
const subscriber = jest.fn();

const close = service.addSubscriber('orgId', 'appId', subscriber);

await setTimeout(50);

expect(subscriber).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenLastCalledWith({ id: 0, data: fakeAppInfo });
expect(common.createHumanitecClient).toHaveBeenCalledTimes(1);

await setTimeout(fetchInterval);

expect(subscriber).toHaveBeenCalledTimes(2);
expect(subscriber).toHaveBeenLastCalledWith({ id: 1, data: fakeAppInfo });
expect(common.createHumanitecClient).toHaveBeenCalledTimes(2);

close();

await setTimeout(fetchInterval * 2);

expect(subscriber).toHaveBeenCalledTimes(2);
expect(common.createHumanitecClient).toHaveBeenCalledTimes(2);
});

it('single subscriber, recovers after an erro', async () => {
returnError = true

const service = new AppInfoService('token', fetchInterval);
const subscriber = jest.fn();

const close = service.addSubscriber('orgId', 'appId', subscriber);

await setTimeout(50);

expect(subscriber).toHaveBeenCalledTimes(1);
expect(subscriber).toHaveBeenLastCalledWith({ id: 0, error: fakeError });
expect(common.createHumanitecClient).toHaveBeenCalledTimes(1);

returnError = false;

await setTimeout(fetchInterval);

expect(subscriber).toHaveBeenCalledTimes(2);
expect(subscriber).toHaveBeenLastCalledWith({ id: 1, data: fakeAppInfo });
expect(common.createHumanitecClient).toHaveBeenCalledTimes(2);

close();

await setTimeout(fetchInterval * 2);

expect(subscriber).toHaveBeenCalledTimes(2);
expect(common.createHumanitecClient).toHaveBeenCalledTimes(2);
});

it('two subscribers', async () => {
const service = new AppInfoService('token', fetchInterval);
const subscriber1 = jest.fn();
const subscriber2 = jest.fn();

const close1 = service.addSubscriber('orgId', 'appId', subscriber1);
const close2 = service.addSubscriber('orgId', 'appId', subscriber2);

await setTimeout(10);

expect(subscriber1).toHaveBeenCalledTimes(1);
expect(subscriber2).toHaveBeenCalledTimes(1);
expect(subscriber1).toHaveBeenLastCalledWith({ id: 0, data: fakeAppInfo });
expect(subscriber2).toHaveBeenLastCalledWith({ id: 0, data: fakeAppInfo });
expect(common.createHumanitecClient).toHaveBeenCalledTimes(1);

await setTimeout(fetchInterval);

expect(subscriber1).toHaveBeenCalledTimes(2);
expect(subscriber1).toHaveBeenLastCalledWith({ id: 1, data: fakeAppInfo });
expect(subscriber2).toHaveBeenCalledTimes(2);
expect(subscriber2).toHaveBeenLastCalledWith({ id: 1, data: fakeAppInfo });
expect(common.createHumanitecClient).toHaveBeenCalledTimes(2);

close1();

await setTimeout(fetchInterval);

expect(subscriber1).toHaveBeenCalledTimes(2);
expect(subscriber2).toHaveBeenCalledTimes(3);
expect(subscriber2).toHaveBeenLastCalledWith({ id: 2, data: fakeAppInfo });
expect(common.createHumanitecClient).toHaveBeenCalledTimes(3);

close2();

await setTimeout(fetchInterval);

expect(subscriber1).toHaveBeenCalledTimes(2);
expect(subscriber2).toHaveBeenCalledTimes(3);
expect(common.createHumanitecClient).toHaveBeenCalledTimes(3);
});
});
45 changes: 23 additions & 22 deletions plugins/humanitec-backend/src/service/app-info-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EventEmitter } from 'events';

import { createHumanitecClient, fetchAppInfo } from '@frontside/backstage-plugin-humanitec-common';

const fetchInterval = 10000;
const defaultFetchInterval = 10000;

export interface AppInfoUpdate {
id: number;
Expand All @@ -16,14 +16,16 @@ export interface AppInfoUpdate {
//
export class AppInfoService {
private emitter: EventEmitter = new EventEmitter();
private pending: Record<string, Promise<any>> = {};
private timeouts: Record<string, NodeJS.Timeout> = {};
private lastData: Record<string, AppInfoUpdate> = {};
private pending: Map<string, Promise<any>> = new Map();
private timeouts: Map<string, NodeJS.Timeout> = new Map();
private lastData: Map<string, AppInfoUpdate> = new Map();

private token: string;
private fetchInterval: number;

constructor(token: string) {
constructor(token: string, fetchInterval = defaultFetchInterval) {
this.token = token;
this.fetchInterval = fetchInterval;
}

addSubscriber(orgId: string, appId: string, subscriber: (data: AppInfoUpdate) => void): () => void {
Expand All @@ -32,49 +34,48 @@ export class AppInfoService {
this.emitter.on(key, subscriber);

// Only fetch app info if a fetch is not pending.
if (!this.pending[key]) {
if (!this.pending.has(key)) {
this.fetchAppInfo(orgId, appId);
} else {
if (this.lastData[key]) {
subscriber(this.lastData[key]);
if (this.lastData.has(key)) {
subscriber(this.lastData.get(key)!);
}
}

// Return a function that removes this subscriber when it's no longer interested.
return () => {
this.emitter.off(key, subscriber);
if (this.emitter.listenerCount(key) === 0 && this.timeouts[key]) {
clearTimeout(this.timeouts[key]);
delete this.pending[key];
delete this.timeouts[key];
delete this.lastData[key];
if (this.emitter.listenerCount(key) === 0 && this.timeouts.has(key)) {
clearTimeout(this.timeouts.get(key)!);
this.timeouts.delete(key);
this.pending.delete(key);
this.lastData.delete(key);
}
};
}

private fetchAppInfo(orgId: string, appId: string): Promise<any> {
private fetchAppInfo(orgId: string, appId: string): void {
const key = `${orgId}:${appId}`;
const client = createHumanitecClient({ token: this.token, orgId });
let id = 0;

this.pending[key] = (async () => {
const update: AppInfoUpdate = { id: id++ };
const id = this.lastData.has(key) ? this.lastData.get(key)!.id + 1 : 0;

this.pending.set(key, (async () => {
const update: AppInfoUpdate = { id: id };
try {
const data = await fetchAppInfo({ client }, appId);
update.data = data;

this.timeouts[key] = setTimeout(()=> this.fetchAppInfo(orgId, appId), fetchInterval);
} catch (error) {
if (error instanceof Error) {
update.error = error;
} else {
update.error = new Error(`${error}`);
}
} finally {
this.timeouts.set(key, setTimeout(() => this.fetchAppInfo(orgId, appId), this.fetchInterval));
this.lastData.set(key, update);
this.emitter.emit(key, update);
this.lastData[key] = update;
}
})();
return this.pending[key];
})());
}
}
8 changes: 2 additions & 6 deletions plugins/humanitec-backend/src/service/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,11 @@ export async function createRouter(
const unsubscribe = appInfoService.addSubscriber(orgId, appId, (data) => {
if (data.error) {
response.write(`event: update-failure\ndata: ${data.error.message}\nid: ${data.id}\n\n`);
flush(response);
logger.error(`Error encountered trying to update environment`, data.error);
response.end();
unsubscribe();

return
} else {
response.write(`event: update-success\ndata: ${JSON.stringify(data.data)}\nid: ${data.id}\n\n`);
}

response.write(`event: update-success\ndata: ${JSON.stringify(data.data)}\nid: ${data.id}\n\n`);
flush(response);
});

Expand Down

0 comments on commit 0e475db

Please sign in to comment.