feat: Introduce generic auxiliary resource support

This commit is contained in:
Joachim Van Herwegen
2021-02-05 10:57:41 +01:00
parent 766e6318ba
commit d6cdd7dbdf
17 changed files with 761 additions and 0 deletions

View File

@@ -22,6 +22,18 @@ export * from './init/LoggerInitializer';
export * from './init/RootContainerInitializer';
export * from './init/ServerInitializer';
// LDP/Authorization
export * from './ldp/auxiliary/AuxiliaryIdentifierStrategy';
export * from './ldp/auxiliary/AuxiliaryStrategy';
export * from './ldp/auxiliary/ComposedAuxiliaryStrategy';
export * from './ldp/auxiliary/LinkMetadataGenerator';
export * from './ldp/auxiliary/MetadataGenerator';
export * from './ldp/auxiliary/RdfValidator';
export * from './ldp/auxiliary/RoutingAuxiliaryIdentifierStrategy';
export * from './ldp/auxiliary/RoutingAuxiliaryStrategy';
export * from './ldp/auxiliary/SuffixAuxiliaryIdentifierStrategy';
export * from './ldp/auxiliary/Validator';
// LDP/HTTP/Metadata
export * from './ldp/http/metadata/AclLinkMetadataWriter';
export * from './ldp/http/metadata/BasicMetadataExtractor';

View File

@@ -0,0 +1,48 @@
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
/**
* A strategy for handling auxiliary related ResourceIdentifiers.
*/
export interface AuxiliaryIdentifierStrategy {
/**
* Returns the identifier of the auxiliary resource corresponding to the given resource.
* This does not guarantee that this auxiliary resource exists.
*
* Should error if there are multiple results: see {@link getAuxiliaryIdentifiers}.
*
* @param identifier - The ResourceIdentifier of which we need the corresponding auxiliary resource.
*
* @returns The ResourceIdentifier of the corresponding auxiliary resource.
*/
getAuxiliaryIdentifier: (identifier: ResourceIdentifier) => ResourceIdentifier;
/**
* Returns all the identifiers of corresponding auxiliary resources.
* This can be used when there are potentially multiple results.
* In the case of a single result this should be an array containing the result of {@link getAuxiliaryIdentifier}.
*
* @param identifier - The ResourceIdentifier of which we need the corresponding auxiliary resources.
*
* @returns The ResourceIdentifiers of the corresponding auxiliary resources.
*/
getAuxiliaryIdentifiers: (identifier: ResourceIdentifier) => ResourceIdentifier[];
/**
* Checks if the input identifier corresponds to an auxiliary resource.
* This does not check if that auxiliary resource exists,
* only if the identifier indicates that there could be an auxiliary resource there.
* @param identifier - Identifier to check.
*
* @returns true if the input identifier points to an auxiliary resource.
*/
isAuxiliaryIdentifier: (identifier: ResourceIdentifier) => boolean;
/**
* Returns the identifier of the resource which this auxiliary resource is referring to.
* This does not guarantee that this resource exists.
* @param identifier - Identifier of the auxiliary resource.
*
* @returns The ResourceIdentifier of the corresponding resource.
*/
getAssociatedIdentifier: (identifier: ResourceIdentifier) => ResourceIdentifier;
}

View File

@@ -0,0 +1,42 @@
import type { Representation } from '../representation/Representation';
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
/**
* A strategy for handling one or more types of auxiliary resources.
* References to "an auxiliary resource" implicitly imply a specific type of auxiliary resources
* supported by this strategy.
*/
export interface AuxiliaryStrategy extends AuxiliaryIdentifierStrategy {
/**
* Whether this auxiliary resource can be deleted when it's in a root storage container.
* @param identifier - Identifier of the auxiliary resource.
*/
requiresRootAuxiliary: (identifier: ResourceIdentifier) => boolean;
/**
* Adds metadata related to this auxiliary resource,
* in case this is required for this type of auxiliary resource.
* The metadata that is added depends on the given identifier being an auxiliary or associated resource:
* the metadata will be used to link to the other one, and potentially add extra typing info.
*
* Used for:
* Solid, §4.3.1: "For any defined auxiliary resource available for a given Solid resource, all representations of
* that resource MUST include an HTTP Link header pointing to the location of each auxiliary resource."
* https://solid.github.io/specification/protocol#auxiliary-resources-server
*
* The above is an example of how that metadata would only be added in case the input is the associated identifier.
*
* @param metadata - Metadata to update.
*/
addMetadata: (metadata: RepresentationMetadata) => Promise<void>;
/**
* Validates if the representation contains valid data for an auxiliary resource.
* Should throw an error in case the data is invalid.
* @param identifier - Identifier of the auxiliary resource.
* @param representation - Representation of the auxiliary resource.
*/
validate: (representation: Representation) => Promise<void>;
}

View File

@@ -0,0 +1,58 @@
import type { Representation } from '../representation/Representation';
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
import type { AuxiliaryStrategy } from './AuxiliaryStrategy';
import type { MetadataGenerator } from './MetadataGenerator';
import type { Validator } from './Validator';
/**
* An {@link AuxiliaryStrategy} that provides its functionality through the combination of
* an {@link AuxiliaryIdentifierStrategy}, {@link MetadataGenerator} and {@link Validator}.
*/
export class ComposedAuxiliaryStrategy implements AuxiliaryStrategy {
private readonly identifierStrategy: AuxiliaryIdentifierStrategy;
private readonly metadataGenerator?: MetadataGenerator;
private readonly validator?: Validator;
private readonly canDelete: boolean;
public constructor(identifierStrategy: AuxiliaryIdentifierStrategy, metadataGenerator?: MetadataGenerator,
validator?: Validator, canDeleteRoot = false) {
this.identifierStrategy = identifierStrategy;
this.metadataGenerator = metadataGenerator;
this.validator = validator;
this.canDelete = canDeleteRoot;
}
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return this.identifierStrategy.getAuxiliaryIdentifier(identifier);
}
public getAuxiliaryIdentifiers(identifier: ResourceIdentifier): ResourceIdentifier[] {
return this.identifierStrategy.getAuxiliaryIdentifiers(identifier);
}
public isAuxiliaryIdentifier(identifier: ResourceIdentifier): boolean {
return this.identifierStrategy.isAuxiliaryIdentifier(identifier);
}
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return this.identifierStrategy.getAssociatedIdentifier(identifier);
}
public requiresRootAuxiliary(): boolean {
return this.canDelete;
}
public async addMetadata(metadata: RepresentationMetadata): Promise<void> {
if (this.metadataGenerator) {
return this.metadataGenerator.handleSafe(metadata);
}
}
public async validate(representation: Representation): Promise<void> {
if (this.validator) {
return this.validator.handleSafe(representation);
}
}
}

View File

@@ -0,0 +1,28 @@
import { namedNode } from '@rdfjs/data-model';
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
import { MetadataGenerator } from './MetadataGenerator';
/**
* Adds a link to the auxiliary resource when called on the associated resource.
* Specifically: <associatedId> <link> <auxiliaryId> will be added.
*
* In case the input is metadata of an auxiliary resource no metadata will be added
*/
export class LinkMetadataGenerator extends MetadataGenerator {
private readonly link: string;
private readonly identifierStrategy: AuxiliaryIdentifierStrategy;
public constructor(link: string, identifierStrategy: AuxiliaryIdentifierStrategy) {
super();
this.link = link;
this.identifierStrategy = identifierStrategy;
}
public async handle(metadata: RepresentationMetadata): Promise<void> {
const identifier = { path: metadata.identifier.value };
if (!this.identifierStrategy.isAuxiliaryIdentifier(identifier)) {
metadata.add(this.link, namedNode(this.identifierStrategy.getAuxiliaryIdentifier(identifier).path));
}
}
}

View File

@@ -0,0 +1,7 @@
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
/**
* Generic interface for classes that add metadata to a RepresentationMetadata.
*/
export abstract class MetadataGenerator extends AsyncHandler<RepresentationMetadata> { }

View File

@@ -0,0 +1,45 @@
import arrayifyStream from 'arrayify-stream';
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { guardedStreamFrom } from '../../util/StreamUtil';
import { BasicRepresentation } from '../representation/BasicRepresentation';
import type { Representation } from '../representation/Representation';
import { Validator } from './Validator';
/**
* Validates a Representation by verifying if the data stream contains valid RDF data.
* It does this by letting the stored RepresentationConverter convert the data.
*/
export class RdfValidator extends Validator {
protected readonly converter: RepresentationConverter;
public constructor(converter: RepresentationConverter) {
super();
this.converter = converter;
}
public async handle(representation: Representation): Promise<void> {
// If the data already is quads format we know it's RDF
if (representation.metadata.contentType === INTERNAL_QUADS) {
return;
}
// eslint-disable-next-line unicorn/expiring-todo-comments
// TODO: Everything below should be part of a utility cloneRepresentation function.
const identifier = { path: representation.metadata.identifier.value };
// Read data in memory first so it does not get lost
const data = await arrayifyStream(representation.data);
const preferences = { type: { [INTERNAL_QUADS]: 1 }};
// Creating new representation since converter might edit metadata
const tempRepresentation = new BasicRepresentation(data, identifier, representation.metadata.contentType);
const result = await this.converter.handleSafe({ identifier, representation: tempRepresentation, preferences });
// Drain stream to make sure data was parsed correctly
await arrayifyStream(result.data);
// Stream has been drained so need to create new stream
representation.data = guardedStreamFrom(data);
}
}

View File

@@ -0,0 +1,44 @@
import { InternalServerError } from '../../util/errors/InternalServerError';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
/**
* An {@link AuxiliaryIdentifierStrategy} that combines multiple AuxiliaryIdentifierStrategies into one.
* Uses `isAuxiliaryIdentifier` to know which strategy to route to.
*/
export class RoutingAuxiliaryIdentifierStrategy implements AuxiliaryIdentifierStrategy {
protected readonly sources: AuxiliaryIdentifierStrategy[];
public constructor(sources: AuxiliaryIdentifierStrategy[]) {
this.sources = sources;
}
public getAuxiliaryIdentifier(): never {
throw new InternalServerError(
'RoutingAuxiliaryIdentifierStrategy has multiple auxiliary strategies and thus no single auxiliary identifier.',
);
}
public getAuxiliaryIdentifiers(identifier: ResourceIdentifier): ResourceIdentifier[] {
return this.sources.flatMap((source): ResourceIdentifier[] => source.getAuxiliaryIdentifiers(identifier));
}
public isAuxiliaryIdentifier(identifier: ResourceIdentifier): boolean {
return this.sources.some((source): boolean => source.isAuxiliaryIdentifier(identifier));
}
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
const source = this.getMatchingSource(identifier);
return source.getAssociatedIdentifier(identifier);
}
protected getMatchingSource(identifier: ResourceIdentifier): AuxiliaryIdentifierStrategy {
const match = this.sources.find((source): boolean => source.isAuxiliaryIdentifier(identifier));
if (!match) {
throw new NotImplementedHttpError(`Could not find an AuxiliaryManager for ${identifier.path}`);
}
return match;
}
}

View File

@@ -0,0 +1,49 @@
import type { Representation } from '../representation/Representation';
import type { RepresentationMetadata } from '../representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
import type { AuxiliaryStrategy } from './AuxiliaryStrategy';
import { RoutingAuxiliaryIdentifierStrategy } from './RoutingAuxiliaryIdentifierStrategy';
/**
* An {@link AuxiliaryStrategy} that combines multiple AuxiliaryStrategies into one.
* Uses `isAuxiliaryIdentifier` to know which strategy to call for which call.
*
* `addMetadata` will either call all strategies if the input is the associated identifier,
* or only the matching strategy if the input is an auxiliary identifier.
*/
export class RoutingAuxiliaryStrategy extends RoutingAuxiliaryIdentifierStrategy implements AuxiliaryStrategy {
protected readonly sources!: AuxiliaryStrategy[];
public constructor(sources: AuxiliaryStrategy[]) {
super(sources);
}
public requiresRootAuxiliary(identifier: ResourceIdentifier): boolean {
const source = this.getMatchingSource(identifier);
return source.requiresRootAuxiliary(identifier);
}
public async addMetadata(metadata: RepresentationMetadata): Promise<void> {
const identifier = { path: metadata.identifier.value };
// Make sure unrelated auxiliary strategies don't add metadata to another auxiliary resource
const match = this.sources.find((source): boolean => source.isAuxiliaryIdentifier(identifier));
if (match) {
await match.addMetadata(metadata);
} else {
for (const source of this.sources) {
await source.addMetadata(metadata);
}
}
}
public async validate(representation: Representation): Promise<void> {
const identifier = { path: representation.metadata.identifier.value };
const source = this.getMatchingSource(identifier);
return source.validate(representation);
}
// Updated with new source typings
protected getMatchingSource(identifier: ResourceIdentifier): AuxiliaryStrategy {
return super.getMatchingSource(identifier) as AuxiliaryStrategy;
}
}

View File

@@ -0,0 +1,41 @@
import { InternalServerError } from '../../util/errors/InternalServerError';
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
import type { AuxiliaryIdentifierStrategy } from './AuxiliaryIdentifierStrategy';
/**
* Helper class that uses a suffix to determine if a resource is an auxiliary resource or not.
* Simple string matching is used, so the dot needs to be included if needed, e.g. ".acl".
*/
export class SuffixAuxiliaryIdentifierStrategy implements AuxiliaryIdentifierStrategy {
protected readonly suffix: string;
public constructor(suffix: string) {
if (suffix.length === 0) {
throw new InternalServerError('Suffix length should be non-zero.');
}
this.suffix = suffix;
}
public getAuxiliaryIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
return { path: `${identifier.path}${this.suffix}` };
}
public getAuxiliaryIdentifiers(identifier: ResourceIdentifier): ResourceIdentifier[] {
return [ this.getAuxiliaryIdentifier(identifier) ];
}
public isAuxiliaryIdentifier(identifier: ResourceIdentifier): boolean {
return identifier.path.endsWith(this.suffix);
}
public getAssociatedIdentifier(identifier: ResourceIdentifier): ResourceIdentifier {
if (!this.isAuxiliaryIdentifier(identifier)) {
throw new InternalServerError(`${identifier.path} does not end on ${this.suffix} so no conversion is possible.`);
}
return { path: identifier.path.slice(0, -this.suffix.length) };
}
public canDeleteRoot(): boolean {
return true;
}
}

View File

@@ -0,0 +1,7 @@
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
import type { Representation } from '../representation/Representation';
/**
* Generic interface for classes that validate Representations in some way.
*/
export abstract class Validator extends AsyncHandler<Representation> { }