diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d141d414..e6b29eac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,7 +122,7 @@ All notable changes to this project will be documented in this file. * [feat: add typed readable](https://github.com/solid/community-server/commit/e0d74fd68af3575f267f8abc87c51a6fbab28d12) * [feat: Add README with architecture links](https://github.com/solid/community-server/commit/aaf3f8e3aa890219e2a147622605ba2b62b729ee) * [feat: add AuthenticatedLdpHandler](https://github.com/solid/community-server/commit/3e2cfaf11ee13c2ae3cb3e46f4df78c13c9d19cf) -* [feat: add CompositeAsyncHandler to support multiple handlers](https://github.com/solid/community-server/commit/4229932a3ac75c2532da4e495e96b779fc5b6c92) +* [feat: add FirstCompositeHandler to support multiple handlers](https://github.com/solid/community-server/commit/4229932a3ac75c2532da4e495e96b779fc5b6c92) * [feat: add custom errors](https://github.com/solid/community-server/commit/57405f3e2695f3a82628e02052695314d656af95) * [feat: add additional supported interfaces](https://github.com/solid/community-server/commit/a4f2b3995c3e8cfeacf5fe3dbbc0eeb8020f9c9e) * [Initial configuration](https://github.com/solid/community-server/commit/b949b6cf5eade549b91731edcd1c4d931537a42e) diff --git a/config/presets/ldp/operation-handler.json b/config/presets/ldp/operation-handler.json index c81dc03f1..3ae8d9011 100644 --- a/config/presets/ldp/operation-handler.json +++ b/config/presets/ldp/operation-handler.json @@ -3,8 +3,8 @@ "@graph": [ { "@id": "urn:solid-server:default:OperationHandler", - "@type": "CompositeAsyncHandler", - "CompositeAsyncHandler:_handlers": [ + "@type": "FirstCompositeHandler", + "FirstCompositeHandler:_handlers": [ { "@type": "DeleteOperationHandler", "DeleteOperationHandler:_store": { diff --git a/config/presets/ldp/permissions-extractor.json b/config/presets/ldp/permissions-extractor.json index 910bf7766..769f210b9 100644 --- a/config/presets/ldp/permissions-extractor.json +++ b/config/presets/ldp/permissions-extractor.json @@ -3,8 +3,8 @@ "@graph": [ { "@id": "urn:solid-server:default:PermissionsExtractor", - "@type": "CompositeAsyncHandler", - "CompositeAsyncHandler:_handlers": [ + "@type": "FirstCompositeHandler", + "FirstCompositeHandler:_handlers": [ { "@type": "MethodPermissionsExtractor" }, diff --git a/config/presets/ldp/request-parser.json b/config/presets/ldp/request-parser.json index 027058807..eb83a20f9 100644 --- a/config/presets/ldp/request-parser.json +++ b/config/presets/ldp/request-parser.json @@ -14,8 +14,8 @@ "@id": "urn:solid-server:default:MetadataExtractor" }, "BasicRequestParser:_bodyParser": { - "@type": "CompositeAsyncHandler", - "CompositeAsyncHandler:_handlers": [ + "@type": "FirstCompositeHandler", + "FirstCompositeHandler:_handlers": [ { "@type": "SparqlUpdateBodyParser" }, diff --git a/config/presets/ldp/response-writer.json b/config/presets/ldp/response-writer.json index 94a92f34d..6c26c73ed 100644 --- a/config/presets/ldp/response-writer.json +++ b/config/presets/ldp/response-writer.json @@ -3,8 +3,8 @@ "@graph": [ { "@id": "urn:solid-server:default:ResponseWriter", - "@type": "CompositeAsyncHandler", - "CompositeAsyncHandler:_handlers": [ + "@type": "FirstCompositeHandler", + "FirstCompositeHandler:_handlers": [ { "@type": "ErrorResponseWriter" }, diff --git a/config/presets/representation-conversion.json b/config/presets/representation-conversion.json index f868d51a5..d98607bf7 100644 --- a/config/presets/representation-conversion.json +++ b/config/presets/representation-conversion.json @@ -26,8 +26,8 @@ { "@id": "urn:solid-server:default:RepresentationConverter", - "@type": "CompositeAsyncHandler", - "CompositeAsyncHandler:_handlers": [ + "@type": "FirstCompositeHandler", + "FirstCompositeHandler:_handlers": [ { "@id": "urn:solid-server:default:RdfToQuadConverter" }, diff --git a/index.ts b/index.ts index abb6ace36..880e77241 100644 --- a/index.ts +++ b/index.ts @@ -139,8 +139,9 @@ export * from './src/util/errors/UnsupportedHttpError'; export * from './src/util/errors/UnsupportedMediaTypeHttpError'; // Util -export * from './src/util/HeaderUtil'; +export * from './src/util/AllVoidCompositeHandler'; export * from './src/util/AsyncHandler'; -export * from './src/util/CompositeAsyncHandler'; +export * from './src/util/FirstCompositeHandler'; +export * from './src/util/HeaderUtil'; export * from './src/util/MetadataController'; export * from './src/util/Util'; diff --git a/src/util/AllVoidCompositeHandler.ts b/src/util/AllVoidCompositeHandler.ts new file mode 100644 index 000000000..b7442ca94 --- /dev/null +++ b/src/util/AllVoidCompositeHandler.ts @@ -0,0 +1,24 @@ +import { AsyncHandler } from './AsyncHandler'; + +/** + * A composite handler that runs all of its handlers independent of their result. + * The `canHandle` check of this handler will always succeed. + */ +export class AllVoidCompositeHandler extends AsyncHandler { + private readonly handlers: AsyncHandler[]; + + public constructor(handlers: AsyncHandler[]) { + super(); + this.handlers = handlers; + } + + public async handle(input: TIn): Promise { + for (const handler of this.handlers) { + try { + await handler.handleSafe(input); + } catch { + // Ignore errors + } + } + } +} diff --git a/src/util/CompositeAsyncHandler.ts b/src/util/FirstCompositeHandler.ts similarity index 96% rename from src/util/CompositeAsyncHandler.ts rename to src/util/FirstCompositeHandler.ts index 4fc2b8c98..aa53c6aa1 100644 --- a/src/util/CompositeAsyncHandler.ts +++ b/src/util/FirstCompositeHandler.ts @@ -10,13 +10,13 @@ import { UnsupportedHttpError } from './errors/UnsupportedHttpError'; * The handlers will be checked in the order they appear in the input array, * allowing for more fine-grained handlers to check before catch-all handlers. */ -export class CompositeAsyncHandler implements AsyncHandler { +export class FirstCompositeHandler implements AsyncHandler { protected readonly logger = getLoggerFor(this); private readonly handlers: AsyncHandler[]; /** - * Creates a new CompositeAsyncHandler that stores the given handlers. + * Creates a new FirstCompositeHandler that stores the given handlers. * @param handlers - Handlers over which it will run. */ public constructor(handlers: AsyncHandler[]) { diff --git a/test/configs/AuthenticatedDataAccessorBasedConfig.ts b/test/configs/AuthenticatedDataAccessorBasedConfig.ts index 5949457a3..de3654c54 100644 --- a/test/configs/AuthenticatedDataAccessorBasedConfig.ts +++ b/test/configs/AuthenticatedDataAccessorBasedConfig.ts @@ -5,7 +5,7 @@ import type { } from '../../index'; import { AuthenticatedLdpHandler, - CompositeAsyncHandler, + FirstCompositeHandler, MethodPermissionsExtractor, RdfToQuadConverter, UnsecureWebIdExtractor, @@ -44,7 +44,7 @@ export class AuthenticatedDataAccessorBasedConfig implements ServerConfig { const requestParser = getBasicRequestParser(); const credentialsExtractor = new UnsecureWebIdExtractor(); - const permissionsExtractor = new CompositeAsyncHandler([ + const permissionsExtractor = new FirstCompositeHandler([ new MethodPermissionsExtractor(), ]); diff --git a/test/configs/BasicHandlersConfig.ts b/test/configs/BasicHandlersConfig.ts index dec1f7a55..4a0b405ce 100644 --- a/test/configs/BasicHandlersConfig.ts +++ b/test/configs/BasicHandlersConfig.ts @@ -3,7 +3,7 @@ import type { HttpHandler, import { AllowEverythingAuthorizer, AuthenticatedLdpHandler, - CompositeAsyncHandler, + FirstCompositeHandler, MethodPermissionsExtractor, QuadToRdfConverter, RawBodyParser, @@ -48,7 +48,7 @@ export class BasicHandlersConfig implements ServerConfig { ]); const credentialsExtractor = new UnsecureWebIdExtractor(); - const permissionsExtractor = new CompositeAsyncHandler([ + const permissionsExtractor = new FirstCompositeHandler([ new MethodPermissionsExtractor(), new SparqlPatchPermissionsExtractor(), ]); diff --git a/test/configs/BasicHandlersWithAclConfig.ts b/test/configs/BasicHandlersWithAclConfig.ts index 49215944c..cb1fc6b8b 100644 --- a/test/configs/BasicHandlersWithAclConfig.ts +++ b/test/configs/BasicHandlersWithAclConfig.ts @@ -2,7 +2,7 @@ import type { HttpHandler, ResourceStore } from '../../index'; import { AuthenticatedLdpHandler, - CompositeAsyncHandler, + FirstCompositeHandler, MethodPermissionsExtractor, RdfToQuadConverter, UnsecureWebIdExtractor, @@ -40,7 +40,7 @@ export class BasicHandlersWithAclConfig implements ServerConfig { const requestParser = getBasicRequestParser(); const credentialsExtractor = new UnsecureWebIdExtractor(); - const permissionsExtractor = new CompositeAsyncHandler([ + const permissionsExtractor = new FirstCompositeHandler([ new MethodPermissionsExtractor(), ]); diff --git a/test/configs/DataAccessorBasedConfig.ts b/test/configs/DataAccessorBasedConfig.ts index 43ca3a397..921d4281e 100644 --- a/test/configs/DataAccessorBasedConfig.ts +++ b/test/configs/DataAccessorBasedConfig.ts @@ -6,7 +6,7 @@ import type { import { AllowEverythingAuthorizer, AuthenticatedLdpHandler, - CompositeAsyncHandler, + FirstCompositeHandler, MethodPermissionsExtractor, QuadToRdfConverter, RawBodyParser, @@ -44,7 +44,7 @@ export class DataAccessorBasedConfig implements ServerConfig { const requestParser = getBasicRequestParser([ new RawBodyParser() ]); const credentialsExtractor = new UnsecureWebIdExtractor(); - const permissionsExtractor = new CompositeAsyncHandler([ + const permissionsExtractor = new FirstCompositeHandler([ new MethodPermissionsExtractor(), ]); const authorizer = new AllowEverythingAuthorizer(); diff --git a/test/configs/Util.ts b/test/configs/Util.ts index b6c7634fe..5c48e82ac 100644 --- a/test/configs/Util.ts +++ b/test/configs/Util.ts @@ -8,6 +8,7 @@ import type { ResponseDescription, HttpResponse, ResponseWriter, + OperationHandler, } from '../../index'; import { AcceptPreferenceParser, @@ -15,7 +16,7 @@ import { BasicRequestParser, BasicResponseWriter, BasicTargetExtractor, - CompositeAsyncHandler, + FirstCompositeHandler, ContentTypeParser, DataAccessorBasedStore, DeleteOperationHandler, @@ -82,8 +83,8 @@ export const getConvertingStore = (store: ResourceStore, converters: RepresentationConverter[], inType?: string): RepresentationConvertingStore => new RepresentationConvertingStore(store, { - inConverter: new CompositeAsyncHandler(converters), - outConverter: new CompositeAsyncHandler(converters), + inConverter: new FirstCompositeHandler(converters), + outConverter: new FirstCompositeHandler(converters), inType, }); @@ -105,7 +106,7 @@ export const getPatchingStore = (store: ResourceStore): PatchingStore => { * * @returns The operation handler. */ -export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler => { +export const getOperationHandler = (store: ResourceStore): OperationHandler => { const handlers = [ new GetOperationHandler(store), new HeadOperationHandler(store), @@ -114,11 +115,11 @@ export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler new PatchOperationHandler(store), new DeleteOperationHandler(store), ]; - return new CompositeAsyncHandler(handlers); + return new FirstCompositeHandler(handlers); }; export const getResponseWriter = (): ResponseWriter => - new CompositeAsyncHandler<{ response: HttpResponse; result: ResponseDescription | Error }, void>([ + new FirstCompositeHandler<{ response: HttpResponse; result: ResponseDescription | Error }, void>([ new ErrorResponseWriter(), new BasicResponseWriter(), ]); @@ -146,7 +147,7 @@ export const getBasicRequestParser = (bodyParsers: BodyParser[] = []): BasicRequ // If no body parser is given (array is empty), default to RawBodyParser bodyParser = new RawBodyParser(); } else { - bodyParser = new CompositeAsyncHandler(bodyParsers); + bodyParser = new FirstCompositeHandler(bodyParsers); } return new BasicRequestParser({ targetExtractor: new BasicTargetExtractor(), diff --git a/test/unit/util/AllVoidCompositeHandler.test.ts b/test/unit/util/AllVoidCompositeHandler.test.ts new file mode 100644 index 000000000..aec3a9beb --- /dev/null +++ b/test/unit/util/AllVoidCompositeHandler.test.ts @@ -0,0 +1,30 @@ +import { AllVoidCompositeHandler } from '../../../src/util/AllVoidCompositeHandler'; +import type { AsyncHandler } from '../../../src/util/AsyncHandler'; + +describe('An AllVoidCompositeHandler', (): void => { + let handler1: AsyncHandler; + let handler2: AsyncHandler; + let composite: AllVoidCompositeHandler; + + beforeEach(async(): Promise => { + handler1 = { handleSafe: jest.fn() } as any; + handler2 = { handleSafe: jest.fn() } as any; + + composite = new AllVoidCompositeHandler([ handler1, handler2 ]); + }); + + it('can handle all input.', async(): Promise => { + await expect(composite.canHandle('test')).resolves.toBeUndefined(); + }); + + it('runs all handlers without caring about their result.', async(): Promise => { + handler1.handleSafe = jest.fn(async(): Promise => { + throw new Error('error'); + }); + await expect(composite.handleSafe('test')).resolves.toBeUndefined(); + expect(handler1.handleSafe).toHaveBeenCalledTimes(1); + expect(handler1.handleSafe).toHaveBeenLastCalledWith('test'); + expect(handler2.handleSafe).toHaveBeenCalledTimes(1); + expect(handler2.handleSafe).toHaveBeenLastCalledWith('test'); + }); +}); diff --git a/test/unit/util/CompositeAsyncHandler.test.ts b/test/unit/util/FirstCompositeHandler.test.ts similarity index 83% rename from test/unit/util/CompositeAsyncHandler.test.ts rename to test/unit/util/FirstCompositeHandler.test.ts index 21e1f2496..79df5249b 100644 --- a/test/unit/util/CompositeAsyncHandler.test.ts +++ b/test/unit/util/FirstCompositeHandler.test.ts @@ -1,19 +1,19 @@ import type { AsyncHandler } from '../../../src/util/AsyncHandler'; -import { CompositeAsyncHandler } from '../../../src/util/CompositeAsyncHandler'; import { HttpError } from '../../../src/util/errors/HttpError'; import { UnsupportedHttpError } from '../../../src/util/errors/UnsupportedHttpError'; +import { FirstCompositeHandler } from '../../../src/util/FirstCompositeHandler'; import { StaticAsyncHandler } from '../../util/StaticAsyncHandler'; -describe('A CompositeAsyncHandler', (): void => { +describe('A FirstCompositeHandler', (): void => { describe('with no handlers', (): void => { it('can never handle data.', async(): Promise => { - const handler = new CompositeAsyncHandler([]); + const handler = new FirstCompositeHandler([]); await expect(handler.canHandle(null)).rejects.toThrow(Error); }); it('errors if its handle function is called.', async(): Promise => { - const handler = new CompositeAsyncHandler([]); + const handler = new FirstCompositeHandler([]); await expect(handler.handle(null)).rejects.toThrow(Error); }); @@ -36,13 +36,13 @@ describe('A CompositeAsyncHandler', (): void => { }); it('can handle data if a handler supports it.', async(): Promise => { - const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]); + const handler = new FirstCompositeHandler([ handlerFalse, handlerTrue ]); await expect(handler.canHandle(null)).resolves.toBeUndefined(); }); it('can not handle data if no handler supports it.', async(): Promise => { - const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]); + const handler = new FirstCompositeHandler([ handlerFalse, handlerFalse ]); await expect(handler.canHandle(null)).rejects.toThrow('[Not supported, Not supported]'); }); @@ -51,13 +51,13 @@ describe('A CompositeAsyncHandler', (): void => { handlerFalse.canHandle = async(): Promise => { throw 'apple'; }; - const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]); + const handler = new FirstCompositeHandler([ handlerFalse, handlerFalse ]); await expect(handler.canHandle(null)).rejects.toThrow('[Unknown error, Unknown error]'); }); it('handles data if a handler supports it.', async(): Promise => { - const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]); + const handler = new FirstCompositeHandler([ handlerFalse, handlerTrue ]); await expect(handler.handle('test')).resolves.toEqual('test'); expect(canHandleFn).toHaveBeenCalledTimes(1); @@ -65,13 +65,13 @@ describe('A CompositeAsyncHandler', (): void => { }); it('errors if the handle function is called but no handler supports the data.', async(): Promise => { - const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]); + const handler = new FirstCompositeHandler([ handlerFalse, handlerFalse ]); await expect(handler.handle('test')).rejects.toThrow('All handlers failed'); }); it('only calls the canHandle function once of its handlers when handleSafe is called.', async(): Promise => { - const handler = new CompositeAsyncHandler([ handlerFalse, handlerTrue ]); + const handler = new FirstCompositeHandler([ handlerFalse, handlerTrue ]); await expect(handler.handleSafe('test')).resolves.toEqual('test'); expect(canHandleFn).toHaveBeenCalledTimes(1); @@ -79,7 +79,7 @@ describe('A CompositeAsyncHandler', (): void => { }); it('throws the canHandle error when calling handleSafe if the data is not supported.', async(): Promise => { - const handler = new CompositeAsyncHandler([ handlerFalse, handlerFalse ]); + const handler = new FirstCompositeHandler([ handlerFalse, handlerFalse ]); await expect(handler.handleSafe(null)).rejects.toThrow('[Not supported, Not supported]'); }); @@ -88,7 +88,7 @@ describe('A CompositeAsyncHandler', (): void => { handlerTrue.canHandle = async(): Promise => { throw new HttpError(401, 'UnauthorizedHttpError'); }; - const handler = new CompositeAsyncHandler([ handlerTrue, handlerTrue ]); + const handler = new FirstCompositeHandler([ handlerTrue, handlerTrue ]); await expect(handler.canHandle(null)).rejects.toMatchObject({ statusCode: 401, @@ -103,7 +103,7 @@ describe('A CompositeAsyncHandler', (): void => { handlerFalse.canHandle = async(): Promise => { throw new Error('Server is crashing!'); }; - const handler = new CompositeAsyncHandler([ handlerTrue, handlerFalse ]); + const handler = new FirstCompositeHandler([ handlerTrue, handlerFalse ]); await expect(handler.canHandle(null)).rejects.toMatchObject({ statusCode: 500, @@ -118,7 +118,7 @@ describe('A CompositeAsyncHandler', (): void => { handlerFalse.canHandle = async(): Promise => { throw new HttpError(415, 'UnsupportedMediaTypeHttpError'); }; - const handler = new CompositeAsyncHandler([ handlerTrue, handlerFalse ]); + const handler = new FirstCompositeHandler([ handlerTrue, handlerFalse ]); await expect(handler.canHandle(null)).rejects.toThrow(UnsupportedHttpError); });