backend-app-api: extract RestrictedIndexedRouter out of root router
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
+51
@@ -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}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
+73
@@ -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<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
-31
@@ -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);
|
||||
});
|
||||
});
|
||||
+6
-48
@@ -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<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user