feat: add implementations of pod-related interfaces

This commit is contained in:
Joachim Van Herwegen
2020-11-27 13:43:44 +01:00
parent 39745ccf22
commit 9653deec7f
9 changed files with 310 additions and 1 deletions

View File

@@ -0,0 +1,56 @@
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
import type { ResourceStore } from '../storage/ResourceStore';
import { ConflictHttpError } from '../util/errors/ConflictHttpError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import type { Agent } from './agent/Agent';
import type { IdentifierGenerator } from './generate/IdentifierGenerator';
import type { ResourcesGenerator } from './generate/ResourcesGenerator';
import type { PodManager } from './PodManager';
/**
* Pod manager that uses an {@link IdentifierGenerator} and {@link ResourcesGenerator}
* to create the default resources and identifier for a new pod.
*/
export class GeneratedPodManager implements PodManager {
protected readonly logger = getLoggerFor(this);
private readonly store: ResourceStore;
private readonly idGenerator: IdentifierGenerator;
private readonly resourcesGenerator: ResourcesGenerator;
public constructor(store: ResourceStore, idGenerator: IdentifierGenerator, resourcesGenerator: ResourcesGenerator) {
this.store = store;
this.idGenerator = idGenerator;
this.resourcesGenerator = resourcesGenerator;
}
/**
* Creates a new pod, pre-populating it with the resources created by the data generator.
* Pod identifiers are created based on the identifier generator.
* Will throw an error if the given identifier already has a resource.
*/
public async createPod(agent: Agent): Promise<ResourceIdentifier> {
const podIdentifier = this.idGenerator.generate(agent.login);
this.logger.info(`Creating pod ${podIdentifier.path}`);
try {
const result = await this.store.getRepresentation(podIdentifier, {});
result.data.destroy();
throw new ConflictHttpError(`There already is a resource at ${podIdentifier.path}`);
} catch (error: unknown) {
// We want the identifier to not exist
if (!(error instanceof NotFoundHttpError)) {
throw error;
}
}
const resources = this.resourcesGenerator.generate(podIdentifier, agent);
let count = 0;
for await (const { identifier, representation } of resources) {
await this.store.setRepresentation(identifier, representation);
count += 1;
}
this.logger.info(`Added ${count} resources to ${podIdentifier.path}`);
return podIdentifier;
}
}

View File

@@ -0,0 +1,48 @@
import type { Representation } from '../../ldp/representation/Representation';
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
import { readableToString } from '../../util/StreamUtil';
import type { Agent } from './Agent';
import { AgentParser } from './AgentParser';
import Dict = NodeJS.Dict;
const requiredKeys: (keyof Agent)[] = [ 'login', 'webId' ];
const optionalKeys: (keyof Agent)[] = [ 'name', 'email' ];
const agentKeys: Set<keyof Agent> = new Set(requiredKeys.concat(optionalKeys));
/**
* A parser that extracts Agent data from a JSON body.
*/
export class AgentJsonParser extends AgentParser {
public async canHandle(input: Representation): Promise<void> {
if (!input.metadata.contentType || !this.isJSON(input.metadata.contentType)) {
throw new NotImplementedHttpError('Only JSON data is supported');
}
}
public async handle(input: Representation): Promise<Agent> {
const result = JSON.parse(await readableToString(input.data));
this.isValidAgent(result);
return result;
}
private isJSON(mediaType: string): boolean {
return mediaType === 'application/json' || mediaType.endsWith('+json');
}
/**
* Checks if all keys in the object are valid Agent keys and if all required keys are there.
*/
private isValidAgent(data: Dict<string>): asserts data is Agent {
for (const key of Object.keys(data)) {
if (!agentKeys.has(key as keyof Agent)) {
throw new BadRequestHttpError(`${key} is not a valid Agent key`);
}
}
for (const key of requiredKeys) {
if (!data[key]) {
throw new BadRequestHttpError(`Input data is missing Agent key ${key}`);
}
}
}
}

View File

@@ -0,0 +1,12 @@
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
/**
* Utility class for generating container identifiers.
*/
export interface IdentifierGenerator {
/**
* Generates container identifiers based on an input slug.
* This is simply string generation, no resource-related checks are run.
*/
generate: (slug: string) => ResourceIdentifier;
}

View File

@@ -0,0 +1,23 @@
import type { Representation } from '../../ldp/representation/Representation';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import Dict = NodeJS.Dict;
export interface Resource {
identifier: ResourceIdentifier;
representation: Representation;
}
/**
* Generator used to create resources relative to a given base identifier.
*/
export interface ResourcesGenerator {
/**
* Generates resources with the given options.
* The output Map should be sorted so that containers always appear before their contents.
* @param location - Base identifier.
* @param options - Options that can be used when generating resources.
*
* @returns A map where the keys are the identifiers and the values the corresponding representations to store.
*/
generate: (location: ResourceIdentifier, options: Dict<string>) => AsyncIterable<Resource>;
}

View File

@@ -0,0 +1,20 @@
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import { ensureTrailingSlash } from '../../util/PathUtil';
import type { IdentifierGenerator } from './IdentifierGenerator';
/**
* Generates identifiers by appending the slug to a stored base identifier.
* Non-alphanumeric characters will be replaced with `-`.
*/
export class SuffixIdentifierGenerator implements IdentifierGenerator {
private readonly base: string;
public constructor(base: string) {
this.base = base;
}
public generate(slug: string): ResourceIdentifier {
const cleanSlug = slug.replace(/\W/gu, '-');
return { path: ensureTrailingSlash(new URL(cleanSlug, this.base).href) };
}
}