From b77786901197004a7bb3431be0ba6773730ebda4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 3 Jan 2023 17:40:45 +0100 Subject: [PATCH] backend-app-api: extract RestrictedIndexedRouter out of root router Signed-off-by: Patrik Oldsberg --- .../RestrictedIndexedRouter.test.ts | 51 +++++++++++++ .../rootHttpRouter/RestrictedIndexedRouter.ts | 73 +++++++++++++++++++ .../rootHttpRouterFactory.test.ts | 31 -------- .../rootHttpRouter/rootHttpRouterFactory.ts | 54 ++------------ 4 files changed, 130 insertions(+), 79 deletions(-) create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.test.ts create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.ts delete mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.test.ts new file mode 100644 index 0000000000..b72e87f77b --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RestrictedIndexedRouter } from './RestrictedIndexedRouter'; + +describe('RestrictedIndexedRouter', () => { + it.each([ + [['/b'], '/a'], + [['/a'], '/aa/b'], + [['/aa'], '/a/b'], + [['/a/b'], '/aa'], + [['/b/a'], '/a'], + [['/a'], '/aa'], + ])(`with existing paths %s, adds %s without conflict`, (existing, added) => { + const router = new RestrictedIndexedRouter(false); + for (const path of existing) { + router.use(path, () => {}); + } + expect(() => router.use(added, () => {})).not.toThrow(); + }); + + it.each([ + [['/a'], '/a', '/a'], + [['/a'], '/a/b', '/a'], + [['/a/b'], '/a', '/a/b'], + ])( + `find conflict when existing paths %s, adds %s`, + (existing, added, conflict) => { + const router = new RestrictedIndexedRouter(false); + for (const path of existing) { + router.use(path, () => {}); + } + expect(() => router.use(added, () => {})).toThrow( + `Path ${added} conflicts with the existing path ${conflict}`, + ); + }, + ); +}); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.ts new file mode 100644 index 0000000000..961277f34d --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/RestrictedIndexedRouter.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RootHttpRouterService } from '@backstage/backend-plugin-api'; +import { Handler, Router } from 'express'; + +function normalizePath(path: string): string { + return path.replace(/\/*$/, '/'); +} + +export class RestrictedIndexedRouter implements RootHttpRouterService { + #indexPath?: false | string; + + #router = Router(); + #namedRoutes = Router(); + #indexRouter = Router(); + #existingPaths = new Array(); + + constructor(indexPath?: false | string) { + this.#indexPath = indexPath; + this.#router.use(this.#namedRoutes); + this.#router.use(this.#indexRouter); + } + + use(path: string, handler: Handler) { + if (path.match(/^[/\s]*$/)) { + throw new Error(`Root router path may not be empty`); + } + const conflictingPath = this.#findConflictingPath(path); + if (conflictingPath) { + throw new Error( + `Path ${path} conflicts with the existing path ${conflictingPath}`, + ); + } + this.#existingPaths.push(path); + this.#namedRoutes.use(path, handler); + + if (this.#indexPath === path) { + this.#indexRouter.use(handler); + } + } + + handler(): Handler { + return this.#router; + } + + #findConflictingPath(newPath: string): string | undefined { + const normalizedNewPath = normalizePath(newPath); + for (const path of this.#existingPaths) { + const normalizedPath = normalizePath(path); + if (normalizedPath.startsWith(normalizedNewPath)) { + return path; + } + if (normalizedNewPath.startsWith(normalizedPath)) { + return path; + } + } + return undefined; + } +} diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts deleted file mode 100644 index 67b5066fb1..0000000000 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { findConflictingPath } from './rootHttpRouterFactory'; - -describe('findConflictingPath', () => { - it('finds conflicts when present', () => { - expect(findConflictingPath(['/a'], '/a')).toBe('/a'); - expect(findConflictingPath(['/b'], '/a')).toBe(undefined); - expect(findConflictingPath(['/a'], '/a/b')).toBe('/a'); - expect(findConflictingPath(['/a'], '/aa/b')).toBe(undefined); - expect(findConflictingPath(['/aa'], '/a/b')).toBe(undefined); - expect(findConflictingPath(['/a/b'], '/a')).toBe('/a/b'); - expect(findConflictingPath(['/a/b'], '/aa')).toBe(undefined); - expect(findConflictingPath(['/b/a'], '/a')).toBe(undefined); - expect(findConflictingPath(['/a'], '/aa')).toBe(undefined); - }); -}); diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts index dae7474793..ffe345f0a0 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts @@ -18,9 +18,9 @@ import { createServiceFactory, coreServices, } from '@backstage/backend-plugin-api'; -import Router from 'express-promise-router'; import { Handler } from 'express'; import { createServiceBuilder } from '@backstage/backend-common'; +import { RestrictedIndexedRouter } from './RestrictedIndexedRouter'; /** * @public @@ -45,10 +45,9 @@ export const rootHttpRouterFactory = createServiceFactory({ lifecycle: coreServices.rootLifecycle, }, async factory({ config, lifecycle }, options?: RootHttpRouterFactoryOptions) { - const indexPath = options?.indexPath ?? '/api/app'; - - const namedRouter = Router(); - const indexRouter = Router(); + const router = new RestrictedIndexedRouter( + options?.indexPath ?? '/api/app', + ); const service = createServiceBuilder(module).loadConfig(config); @@ -56,7 +55,7 @@ export const rootHttpRouterFactory = createServiceFactory({ service.addRouter('', middleware); } - service.addRouter('', namedRouter).addRouter('', indexRouter); + service.addRouter('', router.handler()); const server = await service.start(); // Stop method isn't part of the public API, let's fix that once we move the implementation here. @@ -79,47 +78,6 @@ export const rootHttpRouterFactory = createServiceFactory({ labels: { service: 'rootHttpRouter' }, }); - const existingPaths = new Array(); - - return { - use: (path: string, handler: Handler) => { - if (path.match(/^[/\s]*$/)) { - throw new Error(`Root router path may not be empty`); - } - const conflictingPath = findConflictingPath(existingPaths, path); - if (conflictingPath) { - throw new Error( - `Path ${path} conflicts with the existing path ${conflictingPath}`, - ); - } - existingPaths.push(path); - namedRouter.use(path, handler); - - if (indexPath === path) { - indexRouter.use(handler); - } - }, - }; + return router; }, }); - -function normalizePath(path: string): string { - return path.replace(/\/*$/, '/'); -} - -export function findConflictingPath( - paths: string[], - newPath: string, -): string | undefined { - const normalizedNewPath = normalizePath(newPath); - for (const path of paths) { - const normalizedPath = normalizePath(path); - if (normalizedPath.startsWith(normalizedNewPath)) { - return path; - } - if (normalizedNewPath.startsWith(normalizedPath)) { - return path; - } - } - return undefined; -}