diff --git a/src/storage/MonitoringStore.ts b/src/storage/MonitoringStore.ts new file mode 100644 index 000000000..a4bd1237b --- /dev/null +++ b/src/storage/MonitoringStore.ts @@ -0,0 +1,49 @@ +import { EventEmitter } from 'events'; +import type { Patch } from '../ldp/http/Patch'; +import type { Representation } from '../ldp/representation/Representation'; +import type { RepresentationPreferences } from '../ldp/representation/RepresentationPreferences'; +import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier'; +import type { Conditions } from './Conditions'; +import type { ResourceStore } from './ResourceStore'; + +/** + * Store that notifies listeners of changes to its source + * by emitting a `changed` event. + */ +export class MonitoringStore + extends EventEmitter implements ResourceStore { + private readonly source: T; + + public constructor(source: T) { + super(); + this.source = source; + } + + public async addResource(container: ResourceIdentifier, representation: Representation, + conditions?: Conditions): Promise { + const identifier = await this.source.addResource(container, representation, conditions); + this.emit('changed', identifier); + return identifier; + } + + public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise { + await this.source.deleteResource(identifier, conditions); + this.emit('changed', identifier); + } + + public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences, + conditions?: Conditions): Promise { + return this.source.getRepresentation(identifier, preferences, conditions); + } + + public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise { + await this.source.modifyResource(identifier, patch, conditions); + this.emit('changed', identifier); + } + + public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, + conditions?: Conditions): Promise { + await this.source.setRepresentation(identifier, representation, conditions); + this.emit('changed', identifier); + } +} diff --git a/test/unit/storage/MonitoringStore.test.ts b/test/unit/storage/MonitoringStore.test.ts new file mode 100644 index 000000000..cfd61bcfc --- /dev/null +++ b/test/unit/storage/MonitoringStore.test.ts @@ -0,0 +1,94 @@ +import type { Patch } from '../../../src/ldp/http/Patch'; +import type { Representation } from '../../../src/ldp/representation/Representation'; +import { MonitoringStore } from '../../../src/storage/MonitoringStore'; +import type { ResourceStore } from '../../../src/storage/ResourceStore'; + +describe('A MonitoringStore', (): void => { + let store: MonitoringStore; + let source: ResourceStore; + let changedCallback: () => void; + + beforeEach(async(): Promise => { + source = { + getRepresentation: jest.fn(async(): Promise => 'get'), + addResource: jest.fn(async(): Promise => ({ path: 'newResource' })), + setRepresentation: jest.fn(async(): Promise => undefined), + deleteResource: jest.fn(async(): Promise => undefined), + modifyResource: jest.fn(async(): Promise => undefined), + }; + store = new MonitoringStore(source); + changedCallback = jest.fn(); + store.on('changed', changedCallback); + }); + + afterEach(async(): Promise => { + store.removeListener('changed', changedCallback); + }); + + it('calls getRepresentation directly from the source.', async(): Promise => { + await expect(store.getRepresentation({ path: 'getPath' }, {})).resolves.toBe('get'); + expect(source.getRepresentation).toHaveBeenCalledTimes(1); + expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: 'getPath' }, {}, undefined); + }); + + it('does not fire a change event after completing getRepresentation.', async(): Promise => { + expect(changedCallback).toHaveBeenCalledTimes(0); + await store.getRepresentation({ path: 'getPath' }, {}); + expect(changedCallback).toHaveBeenCalledTimes(0); + }); + + it('calls addResource directly from the source.', async(): Promise => { + await expect(store.addResource({ path: 'addPath' }, {} as Representation)).resolves + .toStrictEqual({ path: 'newResource' }); + expect(source.addResource).toHaveBeenCalledTimes(1); + expect(source.addResource).toHaveBeenLastCalledWith({ path: 'addPath' }, {}, undefined); + }); + + it('fires a change event after completing addResource.', async(): Promise => { + const result = store.addResource({ path: 'addPath' }, {} as Representation); + expect(changedCallback).toHaveBeenCalledTimes(0); + await result; + expect(changedCallback).toHaveBeenCalledTimes(1); + expect(changedCallback).toHaveBeenCalledWith({ path: 'newResource' }); + }); + + it('calls setRepresentation directly from the source.', async(): Promise => { + await expect(store.setRepresentation({ path: 'setPath' }, {} as Representation)).resolves.toBeUndefined(); + expect(source.setRepresentation).toHaveBeenCalledTimes(1); + expect(source.setRepresentation).toHaveBeenLastCalledWith({ path: 'setPath' }, {}, undefined); + }); + + it('fires a change event after completing setRepresentation.', async(): Promise => { + const result = store.setRepresentation({ path: 'setPath' }, {} as Representation); + expect(changedCallback).toHaveBeenCalledTimes(0); + await result; + expect(changedCallback).toHaveBeenCalledTimes(1); + expect(changedCallback).toHaveBeenCalledWith({ path: 'setPath' }); + }); + + it('calls deleteResource directly from the source.', async(): Promise => { + await expect(store.deleteResource({ path: 'deletePath' })).resolves.toBeUndefined(); + expect(source.deleteResource).toHaveBeenCalledTimes(1); + expect(source.deleteResource).toHaveBeenLastCalledWith({ path: 'deletePath' }, undefined); + }); + + it('fires a change event after completing deleteResource.', async(): Promise => { + const result = store.deleteResource({ path: 'deletePath' }); + expect(changedCallback).toHaveBeenCalledTimes(0); + await result; + expect(changedCallback).toHaveBeenCalledTimes(1); + }); + + it('calls modifyResource directly from the source.', async(): Promise => { + await expect(store.modifyResource({ path: 'modifyPath' }, {} as Patch)).resolves.toBeUndefined(); + expect(source.modifyResource).toHaveBeenCalledTimes(1); + expect(source.modifyResource).toHaveBeenLastCalledWith({ path: 'modifyPath' }, {}, undefined); + }); + + it('fires a change event after completing modifyResource.', async(): Promise => { + const result = store.modifyResource({ path: 'modifyPath' }, {} as Patch); + expect(changedCallback).toHaveBeenCalledTimes(0); + await result; + expect(changedCallback).toHaveBeenCalledTimes(1); + }); +});