diff --git a/config/presets/ldp/operation-handler.json b/config/presets/ldp/operation-handler.json index 6ca76e7a1..c81dc03f1 100644 --- a/config/presets/ldp/operation-handler.json +++ b/config/presets/ldp/operation-handler.json @@ -17,6 +17,12 @@ "@id": "urn:solid-server:default:ResourceStore_Patching" } }, + { + "@type": "HeadOperationHandler", + "HeadOperationHandler:_store": { + "@id": "urn:solid-server:default:ResourceStore_Patching" + } + }, { "@type": "PatchOperationHandler", "PatchOperationHandler:_store": { diff --git a/index.ts b/index.ts index f1be352e0..9413f7026 100644 --- a/index.ts +++ b/index.ts @@ -42,6 +42,7 @@ export * from './src/logging/WinstonLoggerFactory'; // LDP/Operations export * from './src/ldp/operations/DeleteOperationHandler'; export * from './src/ldp/operations/GetOperationHandler'; +export * from './src/ldp/operations/HeadOperationHandler'; export * from './src/ldp/operations/Operation'; export * from './src/ldp/operations/OperationHandler'; export * from './src/ldp/operations/PatchOperationHandler'; diff --git a/src/ldp/operations/HeadOperationHandler.ts b/src/ldp/operations/HeadOperationHandler.ts new file mode 100644 index 000000000..b17e497ff --- /dev/null +++ b/src/ldp/operations/HeadOperationHandler.ts @@ -0,0 +1,37 @@ +import { Readable } from 'stream'; +import type { ResourceStore } from '../../storage/ResourceStore'; +import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError'; +import type { Operation } from './Operation'; +import { OperationHandler } from './OperationHandler'; +import type { ResponseDescription } from './ResponseDescription'; + +/** + * Handles HEAD {@link Operation}s. + * Calls the getRepresentation function from a {@link ResourceStore}. + */ +export class HeadOperationHandler extends OperationHandler { + private readonly store: ResourceStore; + + public constructor(store: ResourceStore) { + super(); + this.store = store; + } + + public async canHandle(input: Operation): Promise { + if (input.method !== 'HEAD') { + throw new UnsupportedHttpError('This handler only supports HEAD operations.'); + } + } + + public async handle(input: Operation): Promise { + const body = await this.store.getRepresentation(input.target, input.preferences); + + // Close the Readable as we will not return it. + body.data.destroy(); + body.data = new Readable(); + body.data._read = function(): void { + body.data.push(null); + }; + return { identifier: input.target, body }; + } +} diff --git a/test/configs/Util.ts b/test/configs/Util.ts index e98e7178a..d27f52e67 100644 --- a/test/configs/Util.ts +++ b/test/configs/Util.ts @@ -14,6 +14,7 @@ import { DeleteOperationHandler, FileResourceStore, GetOperationHandler, + HeadOperationHandler, InMemoryResourceStore, InteractionController, MetadataController, @@ -94,6 +95,7 @@ export const getPatchingStore = (store: ResourceStore): PatchingStore => { export const getOperationHandler = (store: ResourceStore): CompositeAsyncHandler => { const handlers = [ new GetOperationHandler(store), + new HeadOperationHandler(store), new PostOperationHandler(store), new PutOperationHandler(store), new PatchOperationHandler(store), diff --git a/test/unit/ldp/operations/HeadOperationHandler.test.ts b/test/unit/ldp/operations/HeadOperationHandler.test.ts new file mode 100644 index 000000000..d0914ed25 --- /dev/null +++ b/test/unit/ldp/operations/HeadOperationHandler.test.ts @@ -0,0 +1,28 @@ +import arrayifyStream from 'arrayify-stream'; +import streamifyArray from 'streamify-array'; +import { HeadOperationHandler } from '../../../../src/ldp/operations/HeadOperationHandler'; +import type { Operation } from '../../../../src/ldp/operations/Operation'; +import type { Representation } from '../../../../src/ldp/representation/Representation'; +import type { ResourceStore } from '../../../../src/storage/ResourceStore'; +import { UnsupportedHttpError } from '../../../../src/util/errors/UnsupportedHttpError'; + +describe('A HeadOperationHandler', (): void => { + const store = { + getRepresentation: async(): Promise => ({ binary: false, data: streamifyArray([ 1, 2, 3 ]) } as + Representation), + } as unknown as ResourceStore; + const handler = new HeadOperationHandler(store); + + it('only supports HEAD operations.', async(): Promise => { + await expect(handler.canHandle({ method: 'HEAD' } as Operation)).resolves.toBeUndefined(); + await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(UnsupportedHttpError); + await expect(handler.canHandle({ method: 'POST' } as Operation)).rejects.toThrow(UnsupportedHttpError); + }); + + it('returns the representation from the store with the input identifier and empty data.', async(): Promise => { + const result = await handler.handle({ target: { path: 'url' }} as Operation); + expect(result.identifier.path).toBe('url'); + expect(result.body?.binary).toBe(false); + await expect(arrayifyStream(result.body!.data)).resolves.toEqual([]); + }); +});