Skip to content

Commit

Permalink
feat: Make stores return modified resources.
Browse files Browse the repository at this point in the history
  • Loading branch information
RubenVerborgh authored and joachimvh committed Feb 24, 2021
1 parent 28c0eb7 commit 6edc255
Show file tree
Hide file tree
Showing 23 changed files with 223 additions and 119 deletions.
18 changes: 10 additions & 8 deletions src/storage/BaseResourceStore.ts
Expand Up @@ -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<ResourceIdentifier> {
public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
conditions?: Conditions): Promise<Representation> {
throw new NotImplementedHttpError();
}

public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<void> {
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
throw new NotImplementedHttpError();
}

public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
conditions?: Conditions): Promise<Representation> {
public async addResource(container: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<ResourceIdentifier> {
throw new NotImplementedHttpError();
}

public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
public async deleteResource(identifier: ResourceIdentifier,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
throw new NotImplementedHttpError();
}

public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<void> {
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
throw new NotImplementedHttpError();
}
}
59 changes: 40 additions & 19 deletions src/storage/DataAccessorBasedStore.ts
Expand Up @@ -134,7 +134,8 @@ export class DataAccessorBasedStore implements ResourceStore {
return newID;
}

public async setRepresentation(identifier: ResourceIdentifier, representation: Representation): Promise<void> {
public async setRepresentation(identifier: ResourceIdentifier, representation: Representation):
Promise<ResourceIdentifier[]> {
this.validateIdentifier(identifier);

// Ensure the representation is supported by the accessor
Expand All @@ -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<void> {
public async modifyResource(): Promise<ResourceIdentifier[]> {
throw new NotImplementedHttpError('Patches are not supported by the default store.');
}

public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
public async deleteResource(identifier: ResourceIdentifier): Promise<ResourceIdentifier[]> {
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,
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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<void> {
createContainers?: boolean): Promise<ResourceIdentifier[]> {
// 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;
Expand All @@ -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 ];
}

/**
Expand Down Expand Up @@ -442,25 +457,29 @@ 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<void[]> {
return Promise.all(identifiers.map(async(identifier): Promise<void> => {
protected async safelyDeleteAuxiliaryResources(identifiers: ResourceIdentifier[]): Promise<ResourceIdentifier[]> {
const deleted: ResourceIdentifier[] = [];
await Promise.all(identifiers.map(async(identifier): Promise<void> => {
try {
await this.accessor.deleteResource(identifier);
deleted.push(identifier);
} catch (error: unknown) {
if (!NotFoundHttpError.isInstance(error)) {
const errorMsg = isNativeError(error) ? error.message : error;
this.logger.error(`Problem deleting auxiliary resource ${identifier.path}: ${errorMsg}`);
}
}
}));
return deleted;
}

/**
* Create containers starting from the root until the given identifier corresponds to an existing container.
* 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<void> {
protected async createRecursiveContainers(container: ResourceIdentifier): Promise<ResourceIdentifier[]> {
// Verify whether the container already exists
try {
const metadata = await this.getNormalizedMetadata(container);
// See #480
Expand All @@ -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 ];
}
}
14 changes: 8 additions & 6 deletions src/storage/LockingResourceStore.ts
Expand Up @@ -48,19 +48,21 @@ export class LockingResourceStore implements AtomicResourceStore {
}

public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<void> {
conditions?: Conditions): Promise<ResourceIdentifier[]> {
return this.locks.withWriteLock(this.getLockIdentifier(identifier),
async(): Promise<void> => this.source.setRepresentation(identifier, representation, conditions));
async(): Promise<ResourceIdentifier[]> => this.source.setRepresentation(identifier, representation, conditions));
}

public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<void> {
public async deleteResource(identifier: ResourceIdentifier,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
return this.locks.withWriteLock(this.getLockIdentifier(identifier),
async(): Promise<void> => this.source.deleteResource(identifier, conditions));
async(): Promise<ResourceIdentifier[]> => this.source.deleteResource(identifier, conditions));
}

public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
return this.locks.withWriteLock(this.getLockIdentifier(identifier),
async(): Promise<void> => this.source.modifyResource(identifier, patch, conditions));
async(): Promise<ResourceIdentifier[]> => this.source.modifyResource(identifier, patch, conditions));
}

/**
Expand Down
18 changes: 12 additions & 6 deletions src/storage/MonitoringStore.ts
Expand Up @@ -33,30 +33,36 @@ export class MonitoringStore<T extends ResourceStore = ResourceStore>
return identifier;
}

public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<void> {
await this.source.deleteResource(identifier, conditions);
public async deleteResource(identifier: ResourceIdentifier,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
const modified = await this.source.deleteResource(identifier, conditions);

// Both the container contents and the resource itself have changed
if (!this.identifierStrategy.isRootContainer(identifier)) {
const container = this.identifierStrategy.getParentContainer(identifier);
this.emit('changed', container);
}
this.emit('changed', identifier);

return modified;
}

public async getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences,
conditions?: Conditions): Promise<Representation> {
return this.source.getRepresentation(identifier, preferences, conditions);
}

public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
await this.source.modifyResource(identifier, patch, conditions);
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
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<void> {
await this.source.setRepresentation(identifier, representation, conditions);
conditions?: Conditions): Promise<ResourceIdentifier[]> {
const modified = await this.source.setRepresentation(identifier, representation, conditions);
this.emit('changed', identifier);
return modified;
}
}
8 changes: 5 additions & 3 deletions src/storage/PassthroughStore.ts
Expand Up @@ -22,7 +22,8 @@ export class PassthroughStore<T extends ResourceStore = ResourceStore> implement
return this.source.addResource(container, representation, conditions);
}

public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<void> {
public async deleteResource(identifier: ResourceIdentifier,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
return this.source.deleteResource(identifier, conditions);
}

Expand All @@ -31,12 +32,13 @@ export class PassthroughStore<T extends ResourceStore = ResourceStore> implement
return this.source.getRepresentation(identifier, preferences, conditions);
}

public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
return this.source.modifyResource(identifier, patch, conditions);
}

public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<void> {
conditions?: Conditions): Promise<ResourceIdentifier[]> {
return this.source.setRepresentation(identifier, representation, conditions);
}
}
3 changes: 2 additions & 1 deletion src/storage/PatchingStore.ts
Expand Up @@ -18,7 +18,8 @@ export class PatchingStore<T extends ResourceStore = ResourceStore> extends Pass
this.patcher = patcher;
}

public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
try {
return await this.source.modifyResource(identifier, patch, conditions);
} catch {
Expand Down
8 changes: 5 additions & 3 deletions src/storage/ReadOnlyStore.ts
Expand Up @@ -20,16 +20,18 @@ export class ReadOnlyStore<T extends ResourceStore = ResourceStore> extends Pass
throw new ForbiddenHttpError();
}

public async deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<void> {
public async deleteResource(identifier: ResourceIdentifier,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
throw new ForbiddenHttpError();
}

public async modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<void> {
public async modifyResource(identifier: ResourceIdentifier, patch: Patch,
conditions?: Conditions): Promise<ResourceIdentifier[]> {
throw new ForbiddenHttpError();
}

public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<void> {
conditions?: Conditions): Promise<ResourceIdentifier[]> {
throw new ForbiddenHttpError();
}
}
2 changes: 1 addition & 1 deletion src/storage/RepresentationConvertingStore.ts
Expand Up @@ -48,7 +48,7 @@ export class RepresentationConvertingStore<T extends ResourceStore = ResourceSto
}

public async setRepresentation(identifier: ResourceIdentifier, representation: Representation,
conditions?: Conditions): Promise<void> {
conditions?: Conditions): Promise<ResourceIdentifier[]> {
representation = await this.inConverter.handleSafe({ identifier, representation, preferences: this.inPreferences });
return this.source.setRepresentation(identifier, representation, conditions);
}
Expand Down

0 comments on commit 6edc255

Please sign in to comment.