From a083d5d38ac27d626ead0bacb0c0b561790a0947 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 29 Dec 2022 15:43:47 +0100 Subject: [PATCH] backend-app-api: add root http router service + adapt http router service Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/api-report.md | 15 ++- .../implementations/httpRouterService.ts | 30 ++---- .../src/services/implementations/index.ts | 2 + .../rootHttpRouterService.test.ts | 31 ++++++ .../implementations/rootHttpRouterService.ts | 102 ++++++++++++++++++ 5 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouterService.test.ts create mode 100644 packages/backend-app-api/src/services/implementations/rootHttpRouterService.ts diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index fa3f3bec2e..d7150fc9e8 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -8,11 +8,13 @@ import { CacheService } from '@backstage/backend-plugin-api'; import { ConfigService } from '@backstage/backend-plugin-api'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; +import { Handler } from 'express'; import { HttpRouterService } from '@backstage/backend-plugin-api'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { RootHttpRouterService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; @@ -69,7 +71,7 @@ export const httpRouterFactory: ( // @public (undocumented) export type HttpRouterFactoryOptions = { - indexPlugin?: string; + pathPrefix?: string; }; // @public @@ -87,6 +89,17 @@ export const permissionsFactory: ( options?: undefined, ) => ServiceFactory; +// @public (undocumented) +export const rootHttpRouterFactory: ( + options?: RootHttpRouterFactoryOptions | undefined, +) => ServiceFactory; + +// @public (undocumented) +export type RootHttpRouterFactoryOptions = { + indexPath?: string | false; + middleware?: Handler[]; +}; + // @public export const rootLifecycleFactory: ( options?: undefined, diff --git a/packages/backend-app-api/src/services/implementations/httpRouterService.ts b/packages/backend-app-api/src/services/implementations/httpRouterService.ts index 2021589a50..78d04c988a 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouterService.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouterService.ts @@ -18,49 +18,33 @@ import { createServiceFactory, coreServices, } from '@backstage/backend-plugin-api'; -import Router from 'express-promise-router'; import { Handler } from 'express'; -import { createServiceBuilder } from '@backstage/backend-common'; /** * @public */ export type HttpRouterFactoryOptions = { /** - * The plugin ID used for the index route. Defaults to 'app' + * The path prefix used for each plugin, defaults to `/api/`. */ - indexPlugin?: string; + pathPrefix?: string; }; /** @public */ export const httpRouterFactory = createServiceFactory({ service: coreServices.httpRouter, deps: { - config: coreServices.config, plugin: coreServices.pluginMetadata, + rootHttpRouter: coreServices.rootHttpRouter, }, - async factory({ config }, options?: HttpRouterFactoryOptions) { - const defaultPluginId = options?.indexPlugin ?? 'app'; - - const apiRouter = Router(); - const rootRouter = Router(); - - const service = createServiceBuilder(module) - .loadConfig(config) - .addRouter('/api', apiRouter) - .addRouter('', rootRouter); - - await service.start(); + async factory({ rootHttpRouter }, options?: HttpRouterFactoryOptions) { + const pathPrefix = options?.pathPrefix ?? '/api/'; return async ({ plugin }) => { - const pluginId = plugin.getId(); + const path = pathPrefix + plugin.getId(); return { use(handler: Handler) { - if (pluginId === defaultPluginId) { - rootRouter.use(handler); - } else { - apiRouter.use(`/${pluginId}`, handler); - } + rootHttpRouter.use(path, handler); }, }; }; diff --git a/packages/backend-app-api/src/services/implementations/index.ts b/packages/backend-app-api/src/services/implementations/index.ts index c87011d570..2365ff36f4 100644 --- a/packages/backend-app-api/src/services/implementations/index.ts +++ b/packages/backend-app-api/src/services/implementations/index.ts @@ -25,6 +25,8 @@ export { schedulerFactory } from './schedulerService'; export { tokenManagerFactory } from './tokenManagerService'; export { urlReaderFactory } from './urlReaderService'; export { httpRouterFactory } from './httpRouterService'; +export { rootHttpRouterFactory } from './rootHttpRouterService'; export { lifecycleFactory } from './lifecycleService'; export { rootLifecycleFactory } from './rootLifecycleService'; export type { HttpRouterFactoryOptions } from './httpRouterService'; +export type { RootHttpRouterFactoryOptions } from './rootHttpRouterService'; diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouterService.test.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouterService.test.ts new file mode 100644 index 0000000000..fea99b0df9 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouterService.test.ts @@ -0,0 +1,31 @@ +/* + * 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 './rootHttpRouterService'; + +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/rootHttpRouterService.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouterService.ts new file mode 100644 index 0000000000..19108a00d9 --- /dev/null +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouterService.ts @@ -0,0 +1,102 @@ +/* + * 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 { + createServiceFactory, + coreServices, +} from '@backstage/backend-plugin-api'; +import Router from 'express-promise-router'; +import { Handler } from 'express'; +import { createServiceBuilder } from '@backstage/backend-common'; + +/** + * @public + */ +export type RootHttpRouterFactoryOptions = { + /** + * The path to forward all unmatched requests to. Defaults to '/api/app' + */ + indexPath?: string | false; + + /** + * Middlewares that are added before all other routes. + */ + middleware?: Handler[]; +}; + +/** @public */ +export const rootHttpRouterFactory = createServiceFactory({ + service: coreServices.rootHttpRouter, + deps: { + config: coreServices.config, + }, + async factory({ config }, options?: RootHttpRouterFactoryOptions) { + const indexPath = options?.indexPath ?? '/api/app'; + + const namedRouter = Router(); + const indexRouter = Router(); + + const service = createServiceBuilder(module).loadConfig(config); + + for (const middleware of options?.middleware ?? []) { + service.addRouter('', middleware); + } + + service.addRouter('', namedRouter).addRouter('', indexRouter); + + await service.start(); + + const existingPaths = new Array(); + + return { + use: (path: string, handler: Handler) => { + 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); + } + }, + }; + }, +}); + +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; +}