From 6edc255707d93cb3b9b7d62323802c6a3ff1a8cb Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Tue, 23 Feb 2021 22:58:39 +0100 Subject: [PATCH] feat: Make stores return modified resources. --- src/storage/BaseResourceStore.ts | 26 +++---- src/storage/DataAccessorBasedStore.ts | 59 ++++++++++------ src/storage/LockingResourceStore.ts | 14 ++-- src/storage/MonitoringStore.ts | 18 +++-- src/storage/PassthroughStore.ts | 8 ++- src/storage/PatchingStore.ts | 3 +- src/storage/ReadOnlyStore.ts | 8 ++- src/storage/RepresentationConvertingStore.ts | 2 +- src/storage/ResourceStore.ts | 67 +++++++++++-------- src/storage/RoutingResourceStore.ts | 9 +-- src/storage/accessors/DataAccessor.ts | 4 +- src/storage/accessors/FileDataAccessor.ts | 9 ++- src/storage/accessors/InMemoryDataAccessor.ts | 3 +- src/storage/accessors/SparqlDataAccessor.ts | 5 +- src/storage/patch/PatchHandler.ts | 3 +- src/storage/patch/SparqlUpdatePatchHandler.ts | 4 +- .../operations/DeleteOperationHandler.test.ts | 2 +- .../operations/PatchOperationHandler.test.ts | 2 +- .../operations/PutOperationHandler.test.ts | 2 +- .../storage/DataAccessorBasedStore.test.ts | 65 +++++++++++++----- .../accessors/FileDataAccessor.test.ts | 20 ++++-- .../accessors/InMemoryDataAccessor.test.ts | 13 ++-- .../accessors/SparqlDataAccessor.test.ts | 6 +- 23 files changed, 228 insertions(+), 124 deletions(-) diff --git a/src/storage/BaseResourceStore.ts b/src/storage/BaseResourceStore.ts index 35e9c1258..6b4766b5f 100644 --- a/src/storage/BaseResourceStore.ts +++ b/src/storage/BaseResourceStore.ts @@ -11,26 +11,28 @@ import type { ResourceStore } from './ResourceStore'; */ /* eslint-disable @typescript-eslint/no-unused-vars */ export class BaseResourceStore implements ResourceStore { - public async addResource(container: ResourceIdentifier, representation: Representation, - conditions?: Conditions): Promise { - throw new NotImplementedHttpError(); - } - - public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise { - throw new NotImplementedHttpError(); - } - public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences, conditions?: Conditions): Promise { throw new NotImplementedHttpError(); } - public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise { + public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, + conditions?: Conditions): Promise { throw new NotImplementedHttpError(); } - public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, - conditions?: Conditions): Promise { + public async addResource(container: ResourceIdentifier, representation: Representation, + conditions?: Conditions): Promise { + throw new NotImplementedHttpError(); + } + + public async deleteResource(identifier: ResourceIdentifier, + conditions?: Conditions): Promise { + throw new NotImplementedHttpError(); + } + + public async modifyResource(identifier: ResourceIdentifier, patch: Patch, + conditions?: Conditions): Promise { throw new NotImplementedHttpError(); } } diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts index 52a08d450..c0cf73759 100644 --- a/src/storage/DataAccessorBasedStore.ts +++ b/src/storage/DataAccessorBasedStore.ts @@ -134,7 +134,8 @@ export class DataAccessorBasedStore implements ResourceStore { return newID; } - public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise { + public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): + Promise { this.validateIdentifier(identifier); // Ensure the representation is supported by the accessor @@ -161,14 +162,14 @@ export class DataAccessorBasedStore implements ResourceStore { } // Potentially have to create containers if it didn't exist yet - await this.writeData(identifier, representation, isContainer, !oldMetadata); + return this.writeData(identifier, representation, isContainer, !oldMetadata); } - public async modifyResource(): Promise { + public async modifyResource(): Promise { throw new NotImplementedHttpError('Patches are not supported by the default store.'); } - public async deleteResource(identifier: ResourceIdentifier): Promise { + public async deleteResource(identifier: ResourceIdentifier): Promise { this.validateIdentifier(identifier); const metadata = await this.accessor.getMetadata(identifier); // Solid, §5.4: "When a DELETE request targets storage’s root container or its associated ACL resource, @@ -199,11 +200,14 @@ export class DataAccessorBasedStore implements ResourceStore { // Solid, §5.4: "When a contained resource is deleted, the server MUST also delete the associated auxiliary // resources" // https://solid.github.io/specification/protocol#deleting-resources + const deleted = []; if (!this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) { - await this.safelyDeleteAuxiliaryResources(this.auxiliaryStrategy.getAuxiliaryIdentifiers(identifier)); + const auxiliaries = this.auxiliaryStrategy.getAuxiliaryIdentifiers(identifier); + deleted.push(...await this.safelyDeleteAuxiliaryResources(auxiliaries)); } - return this.accessor.deleteResource(identifier); + deleted.unshift(...await this.accessor.deleteResource(identifier)); + return deleted; } /** @@ -265,9 +269,11 @@ export class DataAccessorBasedStore implements ResourceStore { * @param representation - Corresponding Representation. * @param isContainer - Is the incoming resource a container? * @param createContainers - Should parent containers (potentially) be created? + * + * @returns Identifiers of resources that were possibly modified. */ protected async writeData(identifier: ResourceIdentifier, representation: Representation, isContainer: boolean, - createContainers?: boolean): Promise { + createContainers?: boolean): Promise { // Make sure the metadata has the correct identifier and correct type quads // Need to do this before handling container data to have the correct identifier const { metadata } = representation; @@ -288,13 +294,22 @@ export class DataAccessorBasedStore implements ResourceStore { // Solid, §5.3: "Servers MUST create intermediate containers and include corresponding containment triples // in container representations derived from the URI path component of PUT and PATCH requests." // https://solid.github.io/specification/protocol#writing-resources - if (createContainers && !this.identifierStrategy.isRootContainer(identifier)) { - await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(identifier)); + const modified = []; + if (!this.identifierStrategy.isRootContainer(identifier)) { + const container = this.identifierStrategy.getParentContainer(identifier); + if (!createContainers) { + modified.push(container); + } else { + const created = await this.createRecursiveContainers(container); + modified.push(...created.length === 0 ? [ container ] : created); + } } await (isContainer ? this.accessor.writeContainer(identifier, representation.metadata) : this.accessor.writeDocument(identifier, representation.data, representation.metadata)); + + return [ ...modified, identifier ]; } /** @@ -442,10 +457,12 @@ export class DataAccessorBasedStore implements ResourceStore { * Deletes the given array of auxiliary identifiers. * Does not throw an error if something goes wrong. */ - protected async safelyDeleteAuxiliaryResources(identifiers: ResourceIdentifier[]): Promise { - return Promise.all(identifiers.map(async(identifier): Promise => { + protected async safelyDeleteAuxiliaryResources(identifiers: ResourceIdentifier[]): Promise { + const deleted: ResourceIdentifier[] = []; + await Promise.all(identifiers.map(async(identifier): Promise => { try { await this.accessor.deleteResource(identifier); + deleted.push(identifier); } catch (error: unknown) { if (!NotFoundHttpError.isInstance(error)) { const errorMsg = isNativeError(error) ? error.message : error; @@ -453,6 +470,7 @@ export class DataAccessorBasedStore implements ResourceStore { } } })); + return deleted; } /** @@ -460,7 +478,8 @@ export class DataAccessorBasedStore implements ResourceStore { * Will throw errors if the identifier of the last existing "container" corresponds to an existing document. * @param container - Identifier of the container which will need to exist. */ - protected async createRecursiveContainers(container: ResourceIdentifier): Promise { + protected async createRecursiveContainers(container: ResourceIdentifier): Promise { + // Verify whether the container already exists try { const metadata = await this.getNormalizedMetadata(container); // See #480 @@ -471,16 +490,18 @@ export class DataAccessorBasedStore implements ResourceStore { if (!isContainerPath(metadata.identifier.value)) { throw new ForbiddenHttpError(`Creating container ${container.path} conflicts with an existing resource.`); } + return []; } catch (error: unknown) { - if (NotFoundHttpError.isInstance(error)) { - // Make sure the parent exists first - if (!this.identifierStrategy.isRootContainer(container)) { - await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container)); - } - await this.writeData(container, new BasicRepresentation([], container), true); - } else { + if (!NotFoundHttpError.isInstance(error)) { throw error; } } + + // Create the container, starting with its parent + const ancestors = this.identifierStrategy.isRootContainer(container) ? + [] : + await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(container)); + await this.writeData(container, new BasicRepresentation([], container), true); + return [ ...ancestors, container ]; } } diff --git a/src/storage/LockingResourceStore.ts b/src/storage/LockingResourceStore.ts index 85b7561b3..2a6e81e5f 100644 --- a/src/storage/LockingResourceStore.ts +++ b/src/storage/LockingResourceStore.ts @@ -48,19 +48,21 @@ export class LockingResourceStore implements AtomicResourceStore { } public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, - conditions?: Conditions): Promise { + conditions?: Conditions): Promise { return this.locks.withWriteLock(this.getLockIdentifier(identifier), - async(): Promise => this.source.setRepresentation(identifier, representation, conditions)); + async(): Promise => this.source.setRepresentation(identifier, representation, conditions)); } - public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise { + public async deleteResource(identifier: ResourceIdentifier, + conditions?: Conditions): Promise { return this.locks.withWriteLock(this.getLockIdentifier(identifier), - async(): Promise => this.source.deleteResource(identifier, conditions)); + async(): Promise => this.source.deleteResource(identifier, conditions)); } - public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise { + public async modifyResource(identifier: ResourceIdentifier, patch: Patch, + conditions?: Conditions): Promise { return this.locks.withWriteLock(this.getLockIdentifier(identifier), - async(): Promise => this.source.modifyResource(identifier, patch, conditions)); + async(): Promise => this.source.modifyResource(identifier, patch, conditions)); } /** diff --git a/src/storage/MonitoringStore.ts b/src/storage/MonitoringStore.ts index 3e2074ece..43647fbbb 100644 --- a/src/storage/MonitoringStore.ts +++ b/src/storage/MonitoringStore.ts @@ -33,8 +33,9 @@ export class MonitoringStore return identifier; } - public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise { - await this.source.deleteResource(identifier, conditions); + public async deleteResource(identifier: ResourceIdentifier, + conditions?: Conditions): Promise { + const modified = await this.source.deleteResource(identifier, conditions); // Both the container contents and the resource itself have changed if (!this.identifierStrategy.isRootContainer(identifier)) { @@ -42,6 +43,8 @@ export class MonitoringStore this.emit('changed', container); } this.emit('changed', identifier); + + return modified; } public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences, @@ -49,14 +52,17 @@ export class MonitoringStore return this.source.getRepresentation(identifier, preferences, conditions); } - public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise { - await this.source.modifyResource(identifier, patch, conditions); + public async modifyResource(identifier: ResourceIdentifier, patch: Patch, + conditions?: Conditions): Promise { + const modified = await this.source.modifyResource(identifier, patch, conditions); this.emit('changed', identifier); + return modified; } public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, - conditions?: Conditions): Promise { - await this.source.setRepresentation(identifier, representation, conditions); + conditions?: Conditions): Promise { + const modified = await this.source.setRepresentation(identifier, representation, conditions); this.emit('changed', identifier); + return modified; } } diff --git a/src/storage/PassthroughStore.ts b/src/storage/PassthroughStore.ts index a24637770..035db57ed 100644 --- a/src/storage/PassthroughStore.ts +++ b/src/storage/PassthroughStore.ts @@ -22,7 +22,8 @@ export class PassthroughStore implement return this.source.addResource(container, representation, conditions); } - public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise { + public async deleteResource(identifier: ResourceIdentifier, + conditions?: Conditions): Promise { return this.source.deleteResource(identifier, conditions); } @@ -31,12 +32,13 @@ export class PassthroughStore implement return this.source.getRepresentation(identifier, preferences, conditions); } - public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise { + public async modifyResource(identifier: ResourceIdentifier, patch: Patch, + conditions?: Conditions): Promise { return this.source.modifyResource(identifier, patch, conditions); } public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, - conditions?: Conditions): Promise { + conditions?: Conditions): Promise { return this.source.setRepresentation(identifier, representation, conditions); } } diff --git a/src/storage/PatchingStore.ts b/src/storage/PatchingStore.ts index 727d40216..94d5a3ef8 100644 --- a/src/storage/PatchingStore.ts +++ b/src/storage/PatchingStore.ts @@ -18,7 +18,8 @@ export class PatchingStore extends Pass this.patcher = patcher; } - public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise { + public async modifyResource(identifier: ResourceIdentifier, patch: Patch, + conditions?: Conditions): Promise { try { return await this.source.modifyResource(identifier, patch, conditions); } catch { diff --git a/src/storage/ReadOnlyStore.ts b/src/storage/ReadOnlyStore.ts index 86de0dce8..7204197ff 100644 --- a/src/storage/ReadOnlyStore.ts +++ b/src/storage/ReadOnlyStore.ts @@ -20,16 +20,18 @@ export class ReadOnlyStore extends Pass throw new ForbiddenHttpError(); } - public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise { + public async deleteResource(identifier: ResourceIdentifier, + conditions?: Conditions): Promise { throw new ForbiddenHttpError(); } - public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise { + public async modifyResource(identifier: ResourceIdentifier, patch: Patch, + conditions?: Conditions): Promise { throw new ForbiddenHttpError(); } public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, - conditions?: Conditions): Promise { + conditions?: Conditions): Promise { throw new ForbiddenHttpError(); } } diff --git a/src/storage/RepresentationConvertingStore.ts b/src/storage/RepresentationConvertingStore.ts index 763ca3e8d..7f3d006a8 100644 --- a/src/storage/RepresentationConvertingStore.ts +++ b/src/storage/RepresentationConvertingStore.ts @@ -48,7 +48,7 @@ export class RepresentationConvertingStore { + conditions?: Conditions): Promise { representation = await this.inConverter.handleSafe({ identifier, representation, preferences: this.inPreferences }); return this.source.setRepresentation(identifier, representation, conditions); } diff --git a/src/storage/ResourceStore.ts b/src/storage/ResourceStore.ts index e35b0bd91..d021d8baa 100644 --- a/src/storage/ResourceStore.ts +++ b/src/storage/ResourceStore.ts @@ -17,12 +17,12 @@ import type { Conditions } from './Conditions'; */ export interface ResourceStore { /** - * Read a resource. + * Retrieves a representation of a resource. * @param identifier - Identifier of the resource to read. - * @param preferences - Representation preferences. - * @param conditions - Optional conditions. + * @param preferences - Preferences indicating desired representations. + * @param conditions - Optional conditions under which to proceed. * - * @returns A promise containing the representation. + * @returns A representation corresponding to the identifier. */ getRepresentation: ( identifier: ResourceIdentifier, @@ -31,12 +31,27 @@ export interface ResourceStore { ) => Promise; /** - * Create a resource. + * Sets or replaces the representation of a resource, + * creating a new resource and intermediary containers as needed. + * @param identifier - Identifier of resource to update. + * @param representation - New representation of the resource. + * @param conditions - Optional conditions under which to proceed. + * + * @returns Identifiers of resources that were possibly modified. + */ + setRepresentation: ( + identifier: ResourceIdentifier, + representation: Representation, + conditions?: Conditions, + ) => Promise; + + /** + * Creates a new resource in the container. * @param container - Container in which to create a resource. * @param representation - Representation of the new resource - * @param conditions - Optional conditions. + * @param conditions - Optional conditions under which to proceed. * - * @returns A promise containing the new identifier. + * @returns The identifier of the newly created resource. */ addResource: ( container: ResourceIdentifier, @@ -45,35 +60,29 @@ export interface ResourceStore { ) => Promise; /** - * Fully update a resource. - * @param identifier - Identifier of resource to update. - * @param representation - New representation of the resource. - * @param conditions - Optional conditions. - * - * @returns A promise resolving when the update is finished. - */ - setRepresentation: ( - identifier: ResourceIdentifier, - representation: Representation, - conditions?: Conditions, - ) => Promise; - - /** - * Delete a resource. + * Deletes a resource. * @param identifier - Identifier of resource to delete. - * @param conditions - Optional conditions. + * @param conditions - Optional conditions under which to proceed. * - * @returns A promise resolving when the delete is finished. + * @returns Identifiers of resources that were possibly modified. */ - deleteResource: (identifier: ResourceIdentifier, conditions?: Conditions) => Promise; + deleteResource: ( + identifier: ResourceIdentifier, + conditions?: Conditions, + ) => Promise; /** - * Partially update a resource. + * Sets or updates the representation of a resource, + * creating a new resource and intermediary containers as needed. * @param identifier - Identifier of resource to update. * @param patch - Description of which parts to update. - * @param conditions - Optional conditions. + * @param conditions - Optional conditions under which to proceed. * - * @returns A promise resolving when the update is finished. + * @returns Identifiers of resources that were possibly modified. */ - modifyResource: (identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions) => Promise; + modifyResource: ( + identifier: ResourceIdentifier, + patch: Patch, + conditions?: Conditions, + ) => Promise; } diff --git a/src/storage/RoutingResourceStore.ts b/src/storage/RoutingResourceStore.ts index 03490894f..2cbc661a8 100644 --- a/src/storage/RoutingResourceStore.ts +++ b/src/storage/RoutingResourceStore.ts @@ -31,16 +31,17 @@ export class RoutingResourceStore implements ResourceStore { } public async setRepresentation(identifier: ResourceIdentifier, representation: Representation, - conditions?: Conditions): Promise { + conditions?: Conditions): Promise { return (await this.getStore(identifier, representation)).setRepresentation(identifier, representation, conditions); } - public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise { + public async deleteResource(identifier: ResourceIdentifier, + conditions?: Conditions): Promise { return (await this.getStore(identifier)).deleteResource(identifier, conditions); } - public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): - Promise { + public async modifyResource(identifier: ResourceIdentifier, patch: Patch, + conditions?: Conditions): Promise { return (await this.getStore(identifier)).modifyResource(identifier, patch, conditions); } diff --git a/src/storage/accessors/DataAccessor.ts b/src/storage/accessors/DataAccessor.ts index 616fb2482..cc72e3ce9 100644 --- a/src/storage/accessors/DataAccessor.ts +++ b/src/storage/accessors/DataAccessor.ts @@ -63,6 +63,8 @@ export interface DataAccessor { * https://solid.github.io/specification/protocol#deleting-resources * * @param identifier - Resource to delete. + * + * @returns Identifiers of resources that were possibly modified. */ - deleteResource: (identifier: ResourceIdentifier) => Promise; + deleteResource: (identifier: ResourceIdentifier) => Promise; } diff --git a/src/storage/accessors/FileDataAccessor.ts b/src/storage/accessors/FileDataAccessor.ts index bdccaa674..1fb49937d 100644 --- a/src/storage/accessors/FileDataAccessor.ts +++ b/src/storage/accessors/FileDataAccessor.ts @@ -117,12 +117,15 @@ export class FileDataAccessor implements DataAccessor { /** * Removes the corresponding file/folder (and metadata file). */ - public async deleteResource(identifier: ResourceIdentifier): Promise { + public async deleteResource(identifier: ResourceIdentifier): Promise { const link = await this.resourceMapper.mapUrlToFilePath(identifier); + const metadataLink = await this.getMetadataLink(link.identifier); const stats = await this.getStats(link.filePath); + const modified: ResourceIdentifier[] = [ identifier ]; try { - await fsPromises.unlink((await this.getMetadataLink(link.identifier)).filePath); + await fsPromises.unlink(metadataLink.filePath); + modified.push(metadataLink.identifier); } catch (error: unknown) { // Ignore if it doesn't exist if (!isSystemError(error) || error.code !== 'ENOENT') { @@ -137,6 +140,8 @@ export class FileDataAccessor implements DataAccessor { } else { throw new NotFoundHttpError(); } + + return modified; } /** diff --git a/src/storage/accessors/InMemoryDataAccessor.ts b/src/storage/accessors/InMemoryDataAccessor.ts index 1938cb430..86ad32177 100644 --- a/src/storage/accessors/InMemoryDataAccessor.ts +++ b/src/storage/accessors/InMemoryDataAccessor.ts @@ -83,13 +83,14 @@ export class InMemoryDataAccessor implements DataAccessor { } } - public async deleteResource(identifier: ResourceIdentifier): Promise { + public async deleteResource(identifier: ResourceIdentifier): Promise { const { parent, name } = this.getParentEntry(identifier); if (!parent.entries[name]) { throw new NotFoundHttpError(); } // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete parent.entries[name]; + return [ identifier ]; } private isDataEntry(entry: CacheEntry): entry is DataEntry { diff --git a/src/storage/accessors/SparqlDataAccessor.ts b/src/storage/accessors/SparqlDataAccessor.ts index d31125941..071ffd5d5 100644 --- a/src/storage/accessors/SparqlDataAccessor.ts +++ b/src/storage/accessors/SparqlDataAccessor.ts @@ -134,9 +134,10 @@ export class SparqlDataAccessor implements DataAccessor { /** * Removes all graph data relevant to the given identifier. */ - public async deleteResource(identifier: ResourceIdentifier): Promise { + public async deleteResource(identifier: ResourceIdentifier): Promise { const { name, parent } = this.getRelatedNames(identifier); - return this.sendSparqlUpdate(this.sparqlDelete(name, parent)); + await this.sendSparqlUpdate(this.sparqlDelete(name, parent)); + return [ identifier ]; } /** diff --git a/src/storage/patch/PatchHandler.ts b/src/storage/patch/PatchHandler.ts index 0a44d8be3..a96b1ae45 100644 --- a/src/storage/patch/PatchHandler.ts +++ b/src/storage/patch/PatchHandler.ts @@ -2,4 +2,5 @@ import type { Patch } from '../../ldp/http/Patch'; import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier'; import { AsyncHandler } from '../../util/handlers/AsyncHandler'; -export abstract class PatchHandler extends AsyncHandler<{identifier: ResourceIdentifier; patch: Patch}> {} +export abstract class PatchHandler + extends AsyncHandler<{identifier: ResourceIdentifier; patch: Patch}, ResourceIdentifier[]> {} diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts index 455c09372..85824ce9d 100644 --- a/src/storage/patch/SparqlUpdatePatchHandler.ts +++ b/src/storage/patch/SparqlUpdatePatchHandler.ts @@ -34,13 +34,15 @@ export class SparqlUpdatePatchHandler extends PatchHandler { } } - public async handle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): Promise { + public async handle(input: {identifier: ResourceIdentifier; patch: SparqlUpdatePatch}): + Promise { // Verify the patch const { identifier, patch } = input; const op = patch.algebra; this.validateUpdate(op); await this.applyPatch(identifier, op); + return [ identifier ]; } private isDeleteInsert(op: Algebra.Operation): op is Algebra.DeleteInsert { diff --git a/test/unit/ldp/operations/DeleteOperationHandler.test.ts b/test/unit/ldp/operations/DeleteOperationHandler.test.ts index a979c7ba6..cbb272278 100644 --- a/test/unit/ldp/operations/DeleteOperationHandler.test.ts +++ b/test/unit/ldp/operations/DeleteOperationHandler.test.ts @@ -7,7 +7,7 @@ describe('A DeleteOperationHandler', (): void => { const store = {} as unknown as ResourceStore; const handler = new DeleteOperationHandler(store); beforeEach(async(): Promise => { - store.deleteResource = jest.fn(async(): Promise => undefined); + store.deleteResource = jest.fn(async(): Promise => undefined); }); it('only supports DELETE operations.', async(): Promise => { diff --git a/test/unit/ldp/operations/PatchOperationHandler.test.ts b/test/unit/ldp/operations/PatchOperationHandler.test.ts index 6767d4218..aed83c541 100644 --- a/test/unit/ldp/operations/PatchOperationHandler.test.ts +++ b/test/unit/ldp/operations/PatchOperationHandler.test.ts @@ -9,7 +9,7 @@ describe('A PatchOperationHandler', (): void => { const store = {} as unknown as ResourceStore; const handler = new PatchOperationHandler(store); beforeEach(async(): Promise => { - store.modifyResource = jest.fn(async(): Promise => undefined); + store.modifyResource = jest.fn(async(): Promise => undefined); }); it('only supports PATCH operations.', async(): Promise => { diff --git a/test/unit/ldp/operations/PutOperationHandler.test.ts b/test/unit/ldp/operations/PutOperationHandler.test.ts index f667ce9f3..de62efa23 100644 --- a/test/unit/ldp/operations/PutOperationHandler.test.ts +++ b/test/unit/ldp/operations/PutOperationHandler.test.ts @@ -10,7 +10,7 @@ describe('A PutOperationHandler', (): void => { const handler = new PutOperationHandler(store); beforeEach(async(): Promise => { // eslint-disable-next-line @typescript-eslint/no-empty-function - store.setRepresentation = jest.fn(async(): Promise => {}); + store.setRepresentation = jest.fn(async(): Promise => {}); }); it('only supports PUT operations.', async(): Promise => { diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts index e9268df12..f1ebacf82 100644 --- a/test/unit/storage/DataAccessorBasedStore.test.ts +++ b/test/unit/storage/DataAccessorBasedStore.test.ts @@ -40,11 +40,11 @@ class SimpleDataAccessor implements DataAccessor { } } - public async deleteResource(identifier: ResourceIdentifier): Promise { + public async deleteResource(identifier: ResourceIdentifier): Promise { this.checkExists(identifier); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.data[identifier.path]; - return undefined; + return [ identifier ]; } public async getData(identifier: ResourceIdentifier): Promise> { @@ -353,8 +353,8 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.contentType = 'text/turtle'; representation.data = guardedStreamFrom([ `<${`${root}`}> a .` ]); - await expect(store.setRepresentation(resourceID, representation)) - .resolves.toBeUndefined(); + await expect(store.setRepresentation(resourceID, representation)).resolves + .toEqual([{ path: `${root}` }]); expect(mock).toHaveBeenCalledTimes(1); expect(mock).toHaveBeenLastCalledWith(resourceID); @@ -383,7 +383,10 @@ describe('A DataAccessorBasedStore', (): void => { it('can write resources.', async(): Promise => { const resourceID = { path: `${root}resource` }; - await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined(); + await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([ + { path: 'http://test.com/' }, + { path: 'http://test.com/resource' }, + ]); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); }); @@ -394,7 +397,10 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.removeAll(RDF.type); representation.metadata.contentType = 'text/turtle'; representation.data = guardedStreamFrom([ `<${`${root}resource/`}> a .` ]); - await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined(); + await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([ + { path: `${root}` }, + { path: `${root}container/` }, + ]); expect(accessor.data[resourceID.path]).toBeTruthy(); expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined(); }); @@ -403,7 +409,10 @@ describe('A DataAccessorBasedStore', (): void => { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete accessor.data[root]; const resourceID = { path: `${root}resource` }; - await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined(); + await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([ + { path: `${root}` }, + { path: `${root}resource` }, + ]); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); }); @@ -416,7 +425,10 @@ describe('A DataAccessorBasedStore', (): void => { representation.data = guardedStreamFrom( [ quad(namedNode(`${root}resource/`), namedNode('a'), namedNode('coolContainer')) ], ); - await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined(); + await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([ + { path: `${root}` }, + { path: `${root}container/` }, + ]); expect(accessor.data[resourceID.path]).toBeTruthy(); expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined(); }); @@ -436,7 +448,11 @@ describe('A DataAccessorBasedStore', (): void => { it('creates recursive containers when needed.', async(): Promise => { const resourceID = { path: `${root}a/b/resource` }; - await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined(); + await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([ + { path: `${root}a/` }, + { path: `${root}a/b/` }, + { path: `${root}a/b/resource` }, + ]); await expect(arrayifyStream(accessor.data[resourceID.path].data)).resolves.toEqual([ resourceData ]); expect(accessor.data[`${root}a/`].metadata.getAll(RDF.type).map((type): string => type.value)) .toContain(LDP.Container); @@ -461,7 +477,9 @@ describe('A DataAccessorBasedStore', (): void => { representation.metadata.removeAll(RDF.type); representation.metadata.contentType = 'text/turtle'; representation.data = guardedStreamFrom([]); - await expect(store.setRepresentation(resourceID, representation)).resolves.toBeUndefined(); + await expect(store.setRepresentation(resourceID, representation)).resolves.toEqual([ + { path: `${root}` }, + ]); expect(accessor.data[resourceID.path]).toBeTruthy(); expect(Object.keys(accessor.data)).toHaveLength(1); expect(accessor.data[resourceID.path].metadata.contentType).toBeUndefined(); @@ -513,7 +531,9 @@ describe('A DataAccessorBasedStore', (): void => { it('will delete resources.', async(): Promise => { accessor.data[`${root}resource`] = representation; - await expect(store.deleteResource({ path: `${root}resource` })).resolves.toBeUndefined(); + await expect(store.deleteResource({ path: `${root}resource` })).resolves.toEqual([ + { path: `${root}resource` }, + ]); expect(accessor.data[`${root}resource`]).toBeUndefined(); }); @@ -522,14 +542,19 @@ describe('A DataAccessorBasedStore', (): void => { accessor.data[`${root}container/`] = new BasicRepresentation(representation.data, storageMetadata); accessor.data[`${root}container/.dummy`] = representation; auxStrategy.isRootRequired = jest.fn().mockReturnValue(true); - await expect(store.deleteResource({ path: `${root}container/.dummy` })).resolves.toBeUndefined(); + await expect(store.deleteResource({ path: `${root}container/.dummy` })).resolves.toEqual([ + { path: `${root}container/.dummy` }, + ]); expect(accessor.data[`${root}container/.dummy`]).toBeUndefined(); }); it('will delete related auxiliary resources.', async(): Promise => { accessor.data[`${root}container/`] = representation; accessor.data[`${root}container/.dummy`] = representation; - await expect(store.deleteResource({ path: `${root}container/` })).resolves.toBeUndefined(); + await expect(store.deleteResource({ path: `${root}container/` })).resolves.toEqual([ + { path: `${root}container/` }, + { path: `${root}container/.dummy` }, + ]); expect(accessor.data[`${root}container/`]).toBeUndefined(); expect(accessor.data[`${root}container/.dummy`]).toBeUndefined(); }); @@ -538,15 +563,18 @@ describe('A DataAccessorBasedStore', (): void => { accessor.data[`${root}resource`] = representation; accessor.data[`${root}resource.dummy`] = representation; const deleteFn = accessor.deleteResource; - accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise => { + accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise => { if (auxStrategy.isAuxiliaryIdentifier(identifier)) { throw new Error('auxiliary error!'); } await deleteFn.call(accessor, identifier); + return [ identifier ]; }); const { logger } = store as any; logger.error = jest.fn(); - await expect(store.deleteResource({ path: `${root}resource` })).resolves.toBeUndefined(); + await expect(store.deleteResource({ path: `${root}resource` })).resolves.toEqual([ + { path: `${root}resource` }, + ]); expect(accessor.data[`${root}resource`]).toBeUndefined(); expect(accessor.data[`${root}resource.dummy`]).not.toBeUndefined(); expect(logger.error).toHaveBeenCalledTimes(1); @@ -559,15 +587,18 @@ describe('A DataAccessorBasedStore', (): void => { accessor.data[`${root}resource`] = representation; accessor.data[`${root}resource.dummy`] = representation; const deleteFn = accessor.deleteResource; - accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise => { + accessor.deleteResource = jest.fn(async(identifier: ResourceIdentifier): Promise => { if (auxStrategy.isAuxiliaryIdentifier(identifier)) { throw 'auxiliary error!'; } await deleteFn.call(accessor, identifier); + return [ identifier ]; }); const { logger } = store as any; logger.error = jest.fn(); - await expect(store.deleteResource({ path: `${root}resource` })).resolves.toBeUndefined(); + await expect(store.deleteResource({ path: `${root}resource` })).resolves.toEqual([ + { path: `${root}resource` }, + ]); expect(accessor.data[`${root}resource`]).toBeUndefined(); expect(accessor.data[`${root}resource.dummy`]).not.toBeUndefined(); expect(logger.error).toHaveBeenCalledTimes(1); diff --git a/test/unit/storage/accessors/FileDataAccessor.test.ts b/test/unit/storage/accessors/FileDataAccessor.test.ts index 7042d97a2..689f3b964 100644 --- a/test/unit/storage/accessors/FileDataAccessor.test.ts +++ b/test/unit/storage/accessors/FileDataAccessor.test.ts @@ -333,7 +333,8 @@ describe('A FileDataAccessor', (): void => { it('deletes the corresponding file for document.', async(): Promise => { cache.data = { resource: 'apple' }; - await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined(); + await expect(accessor.deleteResource({ path: `${base}resource` })).resolves + .toEqual([{ path: `${base}resource` }]); expect(cache.data.resource).toBeUndefined(); }); @@ -344,22 +345,31 @@ describe('A FileDataAccessor', (): void => { it('removes the corresponding folder for containers.', async(): Promise => { cache.data = { container: {}}; - await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined(); + await expect(accessor.deleteResource({ path: `${base}container/` })).resolves + .toEqual([{ path: `${base}container/` }]); expect(cache.data.container).toBeUndefined(); }); it('removes the corresponding metadata.', async(): Promise => { cache.data = { container: { resource: 'apple', 'resource.meta': 'metaApple', '.meta': 'metadata' }}; - await expect(accessor.deleteResource({ path: `${base}container/resource` })).resolves.toBeUndefined(); + await expect(accessor.deleteResource({ path: `${base}container/resource` })).resolves.toEqual([ + { path: `${base}container/resource` }, + { path: `${base}container/resource.meta` }, + ]); expect(cache.data.container.resource).toBeUndefined(); expect(cache.data.container['resource.meta']).toBeUndefined(); - await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined(); + await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toEqual([ + { path: `${base}container/` }, + { path: `${base}container/.meta` }, + ]); expect(cache.data.container).toBeUndefined(); }); it('can delete the root container.', async(): Promise => { cache.data = { }; - await expect(accessor.deleteResource({ path: `${base}` })).resolves.toBeUndefined(); + await expect(accessor.deleteResource({ path: `${base}` })).resolves.toEqual([ + { path: base }, + ]); expect(cache.data).toBeUndefined(); }); }); diff --git a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts index c6efff338..fc855a75e 100644 --- a/test/unit/storage/accessors/InMemoryDataAccessor.test.ts +++ b/test/unit/storage/accessors/InMemoryDataAccessor.test.ts @@ -170,17 +170,20 @@ describe('An InMemoryDataAccessor', (): void => { it('removes the corresponding resource.', async(): Promise => { await expect(accessor.writeDocument({ path: `${base}resource` }, data, metadata)).resolves.toBeUndefined(); await expect(accessor.writeContainer({ path: `${base}container/` }, metadata)).resolves.toBeUndefined(); - await expect(accessor.deleteResource({ path: `${base}resource` })).resolves.toBeUndefined(); - await expect(accessor.deleteResource({ path: `${base}container/` })).resolves.toBeUndefined(); + await expect(accessor.deleteResource({ path: `${base}resource` })).resolves + .toEqual([{ path: 'http://test.com/resource' }]); + await expect(accessor.deleteResource({ path: `${base}container/` })).resolves + .toEqual([{ path: 'http://test.com/container/' }]); await expect(accessor.getMetadata({ path: `${base}resource` })).rejects.toThrow(NotFoundHttpError); await expect(accessor.getMetadata({ path: `${base}container/` })).rejects.toThrow(NotFoundHttpError); }); it('can delete the root container and write to it again.', async(): Promise => { - await expect(accessor.deleteResource({ path: `${base}` })).resolves.toBeUndefined(); - await expect(accessor.getMetadata({ path: `${base}` })).rejects.toThrow(NotFoundHttpError); + await expect(accessor.deleteResource({ path: base })).resolves + .toEqual([{ path: base }]); + await expect(accessor.getMetadata({ path: base })).rejects.toThrow(NotFoundHttpError); await expect(accessor.getMetadata({ path: `${base}test/` })).rejects.toThrow(NotFoundHttpError); - await expect(accessor.writeContainer({ path: `${base}` }, metadata)).resolves.toBeUndefined(); + await expect(accessor.writeContainer({ path: base }, metadata)).resolves.toBeUndefined(); const resultMetadata = await accessor.getMetadata({ path: `${base}` }); expect(resultMetadata.quads()).toBeRdfIsomorphic(metadata.quads()); }); diff --git a/test/unit/storage/accessors/SparqlDataAccessor.test.ts b/test/unit/storage/accessors/SparqlDataAccessor.test.ts index f1742b55b..abfe972df 100644 --- a/test/unit/storage/accessors/SparqlDataAccessor.test.ts +++ b/test/unit/storage/accessors/SparqlDataAccessor.test.ts @@ -210,7 +210,8 @@ describe('A SparqlDataAccessor', (): void => { it('removes all references when deleting a resource.', async(): Promise => { metadata = new RepresentationMetadata({ path: 'http://test.com/container/' }, { [RDF.type]: [ LDP.terms.Resource, LDP.terms.Container ]}); - await expect(accessor.deleteResource({ path: 'http://test.com/container/' })).resolves.toBeUndefined(); + await expect(accessor.deleteResource({ path: 'http://test.com/container/' })).resolves + .toEqual([{ path: 'http://test.com/container/' }]); expect(fetchUpdate).toHaveBeenCalledTimes(1); expect(fetchUpdate.mock.calls[0][0]).toBe(endpoint); @@ -224,7 +225,8 @@ describe('A SparqlDataAccessor', (): void => { it('does not try to remove containment triples when deleting a root container.', async(): Promise => { metadata = new RepresentationMetadata({ path: 'http://test.com/' }, { [RDF.type]: [ LDP.terms.Resource, LDP.terms.Container ]}); - await expect(accessor.deleteResource({ path: 'http://test.com/' })).resolves.toBeUndefined(); + await expect(accessor.deleteResource({ path: 'http://test.com/' })).resolves + .toEqual([{ path: 'http://test.com/' }]); expect(fetchUpdate).toHaveBeenCalledTimes(1); expect(fetchUpdate.mock.calls[0][0]).toBe(endpoint);