diff --git a/src/index.ts b/src/index.ts index b88ad7565..d015f9981 100644 --- a/src/index.ts +++ b/src/index.ts @@ -190,6 +190,7 @@ export * from './storage/patch/PatchHandler'; export * from './storage/patch/SparqlUpdatePatchHandler'; // Storage/Routing +export * from './storage/routing/BaseUrlRouterRule'; export * from './storage/routing/ConvertingRouterRule'; export * from './storage/routing/PreferenceSupport'; export * from './storage/routing/RegexRouterRule'; diff --git a/src/storage/routing/BaseUrlRouterRule.ts b/src/storage/routing/BaseUrlRouterRule.ts new file mode 100644 index 000000000..ab5b6210d --- /dev/null +++ b/src/storage/routing/BaseUrlRouterRule.ts @@ -0,0 +1,48 @@ +import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; +import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError'; +import type { KeyValueStorage } from '../keyvalue/KeyValueStorage'; +import type { ResourceStore } from '../ResourceStore'; +import { RouterRule } from './RouterRule'; + +/** + * Routes requests based on their base url. + * Checks if any of the stored base URLs match the request identifier. + * If there are no matches the base store will be returned if one was configured. + * + * Part of the dynamic pod creation. + * Uses the identifiers that were added to the routing storage. + * @see {@link TemplatedPodGenerator}, {@link ConfigPodInitializer}, {@link ConfigPodManager} + */ +export class BaseUrlRouterRule extends RouterRule { + private readonly baseStore?: ResourceStore; + private readonly stores: KeyValueStorage; + + public constructor(stores: KeyValueStorage, baseStore?: ResourceStore) { + super(); + this.baseStore = baseStore; + this.stores = stores; + } + + public async handle({ identifier }: { identifier: ResourceIdentifier }): Promise { + try { + return await this.findStore(identifier); + } catch (error: unknown) { + if (this.baseStore) { + return this.baseStore; + } + throw error; + } + } + + /** + * Finds the store whose base url key is contained in the given identifier. + */ + private async findStore(identifier: ResourceIdentifier): Promise { + for await (const [ key, store ] of this.stores.entries()) { + if (identifier.path.startsWith(key.path)) { + return store; + } + } + throw new NotFoundHttpError(); + } +} diff --git a/test/unit/storage/routing/BaseUrlRouterRule.test.ts b/test/unit/storage/routing/BaseUrlRouterRule.test.ts new file mode 100644 index 000000000..eb9db10e9 --- /dev/null +++ b/test/unit/storage/routing/BaseUrlRouterRule.test.ts @@ -0,0 +1,39 @@ +import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; +import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage'; +import type { ResourceStore } from '../../../../src/storage/ResourceStore'; +import { BaseUrlRouterRule } from '../../../../src/storage/routing/BaseUrlRouterRule'; +import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError'; + +describe('A BaseUrlRouterRule', (): void => { + let stores: KeyValueStorage; + const baseStore = 'baseStore!' as any; + const aliceIdentifier = { path: 'http://alice.test.com/' }; + const aliceStore = 'aliceStore!' as any; + let rule: BaseUrlRouterRule; + + beforeEach(async(): Promise => { + const map = new Map([[ aliceIdentifier.path, aliceStore ]]); + stores = { + * entries(): any { + for (const [ path, val ] of map.entries()) { + yield [{ path }, val ]; + } + }, + } as any; + + rule = new BaseUrlRouterRule(stores, baseStore); + }); + + it('returns the matching store if the request contains the correct identifier.', async(): Promise => { + await expect(rule.handle({ identifier: { path: 'http://alice.test.com/foo' }})).resolves.toEqual(aliceStore); + }); + + it('returns the base store if there is no matching identifier.', async(): Promise => { + await expect(rule.handle({ identifier: { path: 'http://bob.test.com/foo' }})).resolves.toEqual(baseStore); + }); + + it('errors if there is no match and no base store.', async(): Promise => { + rule = new BaseUrlRouterRule(stores); + await expect(rule.handle({ identifier: { path: 'http://bob.test.com/foo' }})).rejects.toThrow(NotFoundHttpError); + }); +});