feat: Expose a storage description resource for storage containers

This commit is contained in:
Joachim Van Herwegen
2022-09-30 10:21:48 +02:00
parent 3db1921633
commit df2f69f532
15 changed files with 449 additions and 3 deletions

View File

@@ -0,0 +1,43 @@
import type { NamedNode, Quad, Quad_Object, Term } from '@rdfjs/types';
import { DataFactory } from 'n3';
import { stringToTerm } from 'rdf-string';
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { StorageDescriber } from './StorageDescriber';
import quad = DataFactory.quad;
import namedNode = DataFactory.namedNode;
/**
* Adds a fixed set of triples to the storage description resource,
* with the resource identifier as subject.
*/
export class StaticStorageDescriber extends StorageDescriber {
private readonly terms: ReadonlyMap<NamedNode, Quad_Object[]>;
public constructor(terms: Record<string, string | string[]>) {
super();
const termMap = new Map<NamedNode, Quad_Object[]>();
for (const [ predicate, objects ] of Object.entries(terms)) {
const predTerm = stringToTerm(predicate);
if (predTerm.termType !== 'NamedNode') {
throw new Error('Predicate needs to be a named node.');
}
const objTerms = (Array.isArray(objects) ? objects : [ objects ]).map((obj): Term => stringToTerm(obj));
// `stringToTerm` can only generate valid term types
termMap.set(predTerm, objTerms as Quad_Object[]);
}
this.terms = termMap;
}
public async handle(target: ResourceIdentifier): Promise<Quad[]> {
const subject = namedNode(target.path);
return [ ...this.generateTriples(subject) ];
}
private* generateTriples(subject: NamedNode): Iterable<Quad> {
for (const [ predicate, objects ] of this.terms.entries()) {
for (const object of objects) {
yield quad(subject, predicate, object);
}
}
}
}

View File

@@ -0,0 +1,8 @@
import type { Quad } from '@rdfjs/types';
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
/**
* Generates Quads that need to be added to the given storage description resource.
*/
export abstract class StorageDescriber extends AsyncHandler<ResourceIdentifier, Quad[]> {}

View File

@@ -0,0 +1,58 @@
import { OkResponseDescription } from '../../http/output/response/OkResponseDescription';
import type { ResponseDescription } from '../../http/output/response/ResponseDescription';
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
import type { ResourceStore } from '../../storage/ResourceStore';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { ensureTrailingSlash } from '../../util/PathUtil';
import { PIM, RDF } from '../../util/Vocabularies';
import type { OperationHttpHandlerInput } from '../OperationHttpHandler';
import { OperationHttpHandler } from '../OperationHttpHandler';
import type { StorageDescriber } from './StorageDescriber';
/**
* Generates the response for GET requests targeting a storage description resource.
* The suffix needs to match the suffix used to generate storage description resources
* and will be used to verify the container it is linked to is an actual storage.
*/
export class StorageDescriptionHandler extends OperationHttpHandler {
private readonly store: ResourceStore;
private readonly suffix: string;
private readonly converter: RepresentationConverter;
private readonly describer: StorageDescriber;
public constructor(store: ResourceStore, suffix: string, converter: RepresentationConverter,
describer: StorageDescriber) {
super();
this.store = store;
this.suffix = suffix;
this.converter = converter;
this.describer = describer;
}
public async canHandle({ operation: { target, method }}: OperationHttpHandlerInput): Promise<void> {
if (method !== 'GET') {
throw new MethodNotAllowedHttpError([ method ], `Only GET requests can target the storage description.`);
}
const container = { path: ensureTrailingSlash(target.path.slice(0, -this.suffix.length)) };
const representation = await this.store.getRepresentation(container, {});
representation.data.destroy();
if (!representation.metadata.has(RDF.terms.type, PIM.terms.Storage)) {
throw new NotImplementedHttpError(`Only supports descriptions of storage containers.`);
}
await this.describer.canHandle(target);
}
public async handle({ operation: { target, preferences }}: OperationHttpHandlerInput): Promise<ResponseDescription> {
const quads = await this.describer.handle(target);
const representation = new BasicRepresentation(quads, INTERNAL_QUADS);
const converted = await this.converter.handleSafe({ identifier: target, representation, preferences });
return new OkResponseDescription(converted.metadata, converted.data);
}
}