mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Clean up utility functions
This commit is contained in:
@@ -5,12 +5,12 @@ import type { PermissionSet } from '../ldp/permissions/PermissionSet';
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { ContainerManager } from '../storage/ContainerManager';
|
||||
import type { ResourceStore } from '../storage/ResourceStore';
|
||||
import { INTERNAL_QUADS } from '../util/ContentTypes';
|
||||
import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
|
||||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||
import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
|
||||
import { getParentContainer } from '../util/PathUtil';
|
||||
import { ACL, FOAF } from '../util/UriConstants';
|
||||
import type { AclManager } from './AclManager';
|
||||
import type { AuthorizerArgs } from './Authorizer';
|
||||
@@ -25,13 +25,11 @@ export class WebAclAuthorizer extends Authorizer {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly aclManager: AclManager;
|
||||
private readonly containerManager: ContainerManager;
|
||||
private readonly resourceStore: ResourceStore;
|
||||
|
||||
public constructor(aclManager: AclManager, containerManager: ContainerManager, resourceStore: ResourceStore) {
|
||||
public constructor(aclManager: AclManager, resourceStore: ResourceStore) {
|
||||
super();
|
||||
this.aclManager = aclManager;
|
||||
this.containerManager = containerManager;
|
||||
this.resourceStore = resourceStore;
|
||||
}
|
||||
|
||||
@@ -134,7 +132,7 @@ export class WebAclAuthorizer extends Authorizer {
|
||||
}
|
||||
|
||||
this.logger.debug(`Traversing to the parent of ${id.path}`);
|
||||
const parent = await this.containerManager.getContainer(id);
|
||||
const parent = getParentContainer(id);
|
||||
return this.getAclRecursive(parent, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { HttpResponse } from '../../server/HttpResponse';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import { pipeSafe } from '../../util/Util';
|
||||
import { pipeSafely } from '../../util/StreamUtil';
|
||||
import type { MetadataWriter } from './metadata/MetadataWriter';
|
||||
import type { ResponseDescription } from './response/ResponseDescription';
|
||||
import { ResponseWriter } from './ResponseWriter';
|
||||
@@ -34,7 +34,7 @@ export class BasicResponseWriter extends ResponseWriter {
|
||||
input.response.writeHead(input.result.statusCode);
|
||||
|
||||
if (input.result.data) {
|
||||
const pipe = pipeSafe(input.result.data, input.response);
|
||||
const pipe = pipeSafely(input.result.data, input.response);
|
||||
pipe.on('error', (error): void => {
|
||||
this.logger.error(`Writing to HttpResponse failed with message ${error.message}`);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TLSSocket } from 'tls';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { HttpRequest } from '../../server/HttpRequest';
|
||||
import { toCanonicalUriPath } from '../../util/Util';
|
||||
import { toCanonicalUriPath } from '../../util/PathUtil';
|
||||
import type { ResourceIdentifier } from '../representation/ResourceIdentifier';
|
||||
import { TargetExtractor } from './TargetExtractor';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { APPLICATION_SPARQL_UPDATE } from '../../util/ContentTypes';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
|
||||
import { pipeSafe, readableToString } from '../../util/Util';
|
||||
import { pipeSafely, readableToString } from '../../util/StreamUtil';
|
||||
import type { BodyParserArgs } from './BodyParser';
|
||||
import { BodyParser } from './BodyParser';
|
||||
import type { SparqlUpdatePatch } from './SparqlUpdatePatch';
|
||||
@@ -29,8 +29,8 @@ export class SparqlUpdateBodyParser extends BodyParser {
|
||||
// Note that readableObjectMode is only defined starting from Node 12
|
||||
// It is impossible to check if object mode is enabled in Node 10 (without accessing private variables)
|
||||
const options = { objectMode: request.readableObjectMode };
|
||||
const toAlgebraStream = pipeSafe(request, new PassThrough(options));
|
||||
const dataCopy = pipeSafe(request, new PassThrough(options));
|
||||
const toAlgebraStream = pipeSafely(request, new PassThrough(options));
|
||||
const dataCopy = pipeSafely(request, new PassThrough(options));
|
||||
let algebra: Algebra.Operation;
|
||||
try {
|
||||
const sparql = await readableToString(toAlgebraStream);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HttpResponse } from '../../../server/HttpResponse';
|
||||
import { addHeader } from '../../../util/Util';
|
||||
import { addHeader } from '../../../util/HeaderUtil';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { MetadataWriter } from './MetadataWriter';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HttpResponse } from '../../../server/HttpResponse';
|
||||
import { addHeader } from '../../../util/Util';
|
||||
import { addHeader } from '../../../util/HeaderUtil';
|
||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||
import { MetadataWriter } from './MetadataWriter';
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
|
||||
/**
|
||||
* Handles the identification of containers in which a resource is contained.
|
||||
*/
|
||||
export interface ContainerManager {
|
||||
/**
|
||||
* Finds the corresponding container.
|
||||
* Should throw an error if there is no such container (in the case of root).
|
||||
*
|
||||
* @param id - Identifier to find container of.
|
||||
*
|
||||
* @returns The identifier of the container this resource is in.
|
||||
*/
|
||||
getContainer: (id: ResourceIdentifier) => Promise<ResourceIdentifier>;
|
||||
}
|
||||
@@ -12,11 +12,11 @@ import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpEr
|
||||
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
|
||||
import { NotImplementedError } from '../util/errors/NotImplementedError';
|
||||
import { UnsupportedHttpError } from '../util/errors/UnsupportedHttpError';
|
||||
import type { MetadataController } from '../util/MetadataController';
|
||||
import { ensureTrailingSlash, getParentContainer, trimTrailingSlashes } from '../util/PathUtil';
|
||||
import { parseQuads } from '../util/QuadUtil';
|
||||
import { generateResourceQuads } from '../util/ResourceUtil';
|
||||
import { CONTENT_TYPE, HTTP, LDP, RDF } from '../util/UriConstants';
|
||||
import { ensureTrailingSlash, trimTrailingSlashes } from '../util/Util';
|
||||
import type { DataAccessor } from './accessors/DataAccessor';
|
||||
import type { ContainerManager } from './ContainerManager';
|
||||
import type { ResourceStore } from './ResourceStore';
|
||||
|
||||
/**
|
||||
@@ -45,15 +45,10 @@ import type { ResourceStore } from './ResourceStore';
|
||||
export class DataAccessorBasedStore implements ResourceStore {
|
||||
private readonly accessor: DataAccessor;
|
||||
private readonly base: string;
|
||||
private readonly metadataController: MetadataController;
|
||||
private readonly containerManager: ContainerManager;
|
||||
|
||||
public constructor(accessor: DataAccessor, base: string, metadataController: MetadataController,
|
||||
containerManager: ContainerManager) {
|
||||
public constructor(accessor: DataAccessor, base: string) {
|
||||
this.accessor = accessor;
|
||||
this.base = ensureTrailingSlash(base);
|
||||
this.metadataController = metadataController;
|
||||
this.containerManager = containerManager;
|
||||
}
|
||||
|
||||
public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
|
||||
@@ -219,13 +214,13 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
}
|
||||
|
||||
if (createContainers) {
|
||||
await this.createRecursiveContainers(await this.containerManager.getContainer(identifier));
|
||||
await this.createRecursiveContainers(getParentContainer(identifier));
|
||||
}
|
||||
|
||||
// Make sure the metadata has the correct identifier and correct type quads
|
||||
const { metadata } = representation;
|
||||
metadata.identifier = DataFactory.namedNode(identifier.path);
|
||||
metadata.addQuads(this.metadataController.generateResourceQuads(metadata.identifier, isContainer));
|
||||
metadata.addQuads(generateResourceQuads(metadata.identifier, isContainer));
|
||||
|
||||
await (isContainer ?
|
||||
this.accessor.writeContainer(identifier, representation.metadata) :
|
||||
@@ -241,7 +236,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
protected async handleContainerData(representation: Representation): Promise<void> {
|
||||
let quads: Quad[];
|
||||
try {
|
||||
quads = await this.metadataController.parseQuads(representation.data);
|
||||
quads = await parseQuads(representation.data);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
throw new UnsupportedHttpError(`Can only create containers with RDF data. ${error.message}`);
|
||||
@@ -349,7 +344,7 @@ export class DataAccessorBasedStore implements ResourceStore {
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof NotFoundHttpError) {
|
||||
// Make sure the parent exists first
|
||||
await this.createRecursiveContainers(await this.containerManager.getContainer(container));
|
||||
await this.createRecursiveContainers(getParentContainer(container));
|
||||
await this.writeData(container, this.getEmptyContainerRepresentation(container), true);
|
||||
} else {
|
||||
throw error;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
encodeUriPathComponents,
|
||||
ensureTrailingSlash,
|
||||
trimTrailingSlashes,
|
||||
} from '../util/Util';
|
||||
} from '../util/PathUtil';
|
||||
import type { FileIdentifierMapper, ResourceLink } from './FileIdentifierMapper';
|
||||
|
||||
const { join: joinPath, normalize: normalizePath } = posix;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { ensureTrailingSlash } from '../util/Util';
|
||||
import type { ContainerManager } from './ContainerManager';
|
||||
|
||||
/**
|
||||
* Determines containers based on URL decomposition.
|
||||
*/
|
||||
export class UrlContainerManager implements ContainerManager {
|
||||
private readonly base: string;
|
||||
|
||||
public constructor(base: string) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
public async getContainer(id: ResourceIdentifier): Promise<ResourceIdentifier> {
|
||||
const path = this.canonicalUrl(id.path);
|
||||
if (this.base === path) {
|
||||
throw new Error('Root does not have a container');
|
||||
}
|
||||
|
||||
const parentPath = new URL('..', path).toString();
|
||||
|
||||
// This probably means there is an issue with the root
|
||||
if (parentPath === path) {
|
||||
throw new Error('URL root reached');
|
||||
}
|
||||
|
||||
return { path: parentPath };
|
||||
}
|
||||
|
||||
private canonicalUrl(path: string): string {
|
||||
return ensureTrailingSlash(path.toString());
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
|
||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||
import { isSystemError } from '../../util/errors/SystemError';
|
||||
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
|
||||
import type { MetadataController } from '../../util/MetadataController';
|
||||
import { parseQuads, pushQuad, serializeQuads } from '../../util/QuadUtil';
|
||||
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
|
||||
import { CONTENT_TYPE, DCTERMS, POSIX, RDF, XSD } from '../../util/UriConstants';
|
||||
import { toNamedNode, toTypedLiteral } from '../../util/UriUtil';
|
||||
import { pushQuad } from '../../util/Util';
|
||||
import type { FileIdentifierMapper, ResourceLink } from '../FileIdentifierMapper';
|
||||
import type { DataAccessor } from './DataAccessor';
|
||||
|
||||
@@ -26,11 +26,9 @@ const { join: joinPath } = posix;
|
||||
*/
|
||||
export class FileDataAccessor implements DataAccessor {
|
||||
private readonly resourceMapper: FileIdentifierMapper;
|
||||
private readonly metadataController: MetadataController;
|
||||
|
||||
public constructor(resourceMapper: FileIdentifierMapper, metadataController: MetadataController) {
|
||||
public constructor(resourceMapper: FileIdentifierMapper) {
|
||||
this.resourceMapper = resourceMapper;
|
||||
this.metadataController = metadataController;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +216,7 @@ export class FileDataAccessor implements DataAccessor {
|
||||
|
||||
// Write metadata to file if there are quads remaining
|
||||
if (quads.length > 0) {
|
||||
const serializedMetadata = this.metadataController.serializeQuads(quads);
|
||||
const serializedMetadata = serializeQuads(quads);
|
||||
await this.writeDataFile(metadataPath, serializedMetadata);
|
||||
wroteMetadata = true;
|
||||
|
||||
@@ -247,7 +245,7 @@ export class FileDataAccessor implements DataAccessor {
|
||||
Promise<RepresentationMetadata> {
|
||||
const metadata = new RepresentationMetadata(link.identifier.path)
|
||||
.addQuads(await this.getRawMetadata(link.identifier));
|
||||
metadata.addQuads(this.metadataController.generateResourceQuads(metadata.identifier as NamedNode, isContainer));
|
||||
metadata.addQuads(generateResourceQuads(metadata.identifier as NamedNode, isContainer));
|
||||
metadata.addQuads(this.generatePosixQuads(metadata.identifier as NamedNode, stats));
|
||||
return metadata;
|
||||
}
|
||||
@@ -266,7 +264,7 @@ export class FileDataAccessor implements DataAccessor {
|
||||
await fsPromises.lstat(metadataPath);
|
||||
|
||||
const readMetadataStream = createReadStream(metadataPath);
|
||||
return await this.metadataController.parseQuads(readMetadataStream);
|
||||
return await parseQuads(readMetadataStream);
|
||||
} catch (error: unknown) {
|
||||
// Metadata file doesn't exist so lets keep `rawMetaData` an empty array.
|
||||
if (!isSystemError(error) || error.code !== 'ENOENT') {
|
||||
@@ -306,13 +304,13 @@ export class FileDataAccessor implements DataAccessor {
|
||||
|
||||
// Generate metadata of this specific child
|
||||
const subject = DataFactory.namedNode(childLink.identifier.path);
|
||||
quads.push(...this.metadataController.generateResourceQuads(subject, childStats.isDirectory()));
|
||||
quads.push(...generateResourceQuads(subject, childStats.isDirectory()));
|
||||
quads.push(...this.generatePosixQuads(subject, childStats));
|
||||
childURIs.push(childLink.identifier.path);
|
||||
}
|
||||
|
||||
// Generate containment metadata
|
||||
const containsQuads = this.metadataController.generateContainerContainsResourceQuads(
|
||||
const containsQuads = generateContainmentQuads(
|
||||
DataFactory.namedNode(link.identifier.path), childURIs,
|
||||
);
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { NamedNode } from 'rdf-js';
|
||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||
import type { MetadataController } from '../../util/MetadataController';
|
||||
import { ensureTrailingSlash } from '../../util/Util';
|
||||
import { ensureTrailingSlash } from '../../util/PathUtil';
|
||||
import { generateContainmentQuads, generateResourceQuads } from '../../util/ResourceUtil';
|
||||
import type { DataAccessor } from './DataAccessor';
|
||||
|
||||
interface DataEntry {
|
||||
@@ -43,14 +43,12 @@ class ArrayReadable extends Readable {
|
||||
export class InMemoryDataAccessor implements DataAccessor {
|
||||
private readonly base: string;
|
||||
private readonly store: ContainerEntry;
|
||||
private readonly metadataController: MetadataController;
|
||||
|
||||
public constructor(base: string, metadataController: MetadataController) {
|
||||
public constructor(base: string) {
|
||||
this.base = ensureTrailingSlash(base);
|
||||
this.metadataController = metadataController;
|
||||
|
||||
const metadata = new RepresentationMetadata(this.base);
|
||||
metadata.addQuads(this.metadataController.generateResourceQuads(DataFactory.namedNode(this.base), true));
|
||||
metadata.addQuads(generateResourceQuads(DataFactory.namedNode(this.base), true));
|
||||
this.store = { entries: {}, metadata };
|
||||
}
|
||||
|
||||
@@ -161,8 +159,7 @@ export class InMemoryDataAccessor implements DataAccessor {
|
||||
if (!this.isDataEntry(entry)) {
|
||||
const childNames = Object.keys(entry.entries).map((name): string =>
|
||||
`${identifier.path}${name}${this.isDataEntry(entry.entries[name]) ? '' : '/'}`);
|
||||
const quads = this.metadataController
|
||||
.generateContainerContainsResourceQuads(metadata.identifier as NamedNode, childNames);
|
||||
const quads = generateContainmentQuads(metadata.identifier as NamedNode, childNames);
|
||||
metadata.addQuads(quads);
|
||||
}
|
||||
return metadata;
|
||||
|
||||
@@ -23,11 +23,10 @@ import { ConflictHttpError } from '../../util/errors/ConflictHttpError';
|
||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import { UnsupportedMediaTypeHttpError } from '../../util/errors/UnsupportedMediaTypeHttpError';
|
||||
import type { MetadataController } from '../../util/MetadataController';
|
||||
import { ensureTrailingSlash, getParentContainer } from '../../util/PathUtil';
|
||||
import { generateResourceQuads } from '../../util/ResourceUtil';
|
||||
import { CONTENT_TYPE, LDP } from '../../util/UriConstants';
|
||||
import { toNamedNode } from '../../util/UriUtil';
|
||||
import { ensureTrailingSlash } from '../../util/Util';
|
||||
import type { ContainerManager } from '../ContainerManager';
|
||||
import type { DataAccessor } from './DataAccessor';
|
||||
|
||||
const { defaultGraph, namedNode, quad, variable } = DataFactory;
|
||||
@@ -48,17 +47,12 @@ export class SparqlDataAccessor implements DataAccessor {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
private readonly endpoint: string;
|
||||
private readonly base: string;
|
||||
private readonly containerManager: ContainerManager;
|
||||
private readonly metadataController: MetadataController;
|
||||
private readonly fetcher: SparqlEndpointFetcher;
|
||||
private readonly generator: SparqlGenerator;
|
||||
|
||||
public constructor(endpoint: string, base: string, containerManager: ContainerManager,
|
||||
metadataController: MetadataController) {
|
||||
public constructor(endpoint: string, base: string) {
|
||||
this.endpoint = endpoint;
|
||||
this.base = ensureTrailingSlash(base);
|
||||
this.containerManager = containerManager;
|
||||
this.metadataController = metadataController;
|
||||
this.fetcher = new SparqlEndpointFetcher();
|
||||
this.generator = new Generator();
|
||||
}
|
||||
@@ -103,7 +97,7 @@ export class SparqlDataAccessor implements DataAccessor {
|
||||
|
||||
// Need to generate type metadata for the root container since it's not stored
|
||||
if (identifier.path === this.base) {
|
||||
metadata.addQuads(this.metadataController.generateResourceQuads(name, true));
|
||||
metadata.addQuads(generateResourceQuads(name, true));
|
||||
}
|
||||
|
||||
return metadata;
|
||||
@@ -113,7 +107,7 @@ export class SparqlDataAccessor implements DataAccessor {
|
||||
* Writes the given metadata for the container.
|
||||
*/
|
||||
public async writeContainer(identifier: ResourceIdentifier, metadata: RepresentationMetadata): Promise<void> {
|
||||
const { name, parent } = await this.getRelatedNames(identifier);
|
||||
const { name, parent } = this.getRelatedNames(identifier);
|
||||
return this.sendSparqlUpdate(this.sparqlInsert(name, parent, metadata));
|
||||
}
|
||||
|
||||
@@ -125,7 +119,7 @@ export class SparqlDataAccessor implements DataAccessor {
|
||||
if (this.isMetadataIdentifier(identifier)) {
|
||||
throw new ConflictHttpError('Not allowed to create NamedNodes with the metadata extension.');
|
||||
}
|
||||
const { name, parent } = await this.getRelatedNames(identifier);
|
||||
const { name, parent } = this.getRelatedNames(identifier);
|
||||
|
||||
const triples = await arrayifyStream(data) as Quad[];
|
||||
const def = defaultGraph();
|
||||
@@ -143,15 +137,15 @@ export class SparqlDataAccessor implements DataAccessor {
|
||||
* Removes all graph data relevant to the given identifier.
|
||||
*/
|
||||
public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
|
||||
const { name, parent } = await this.getRelatedNames(identifier);
|
||||
const { name, parent } = this.getRelatedNames(identifier);
|
||||
return this.sendSparqlUpdate(this.sparqlDelete(name, parent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get named nodes corresponding to the identifier and its parent container.
|
||||
*/
|
||||
private async getRelatedNames(identifier: ResourceIdentifier): Promise<{ name: NamedNode; parent: NamedNode }> {
|
||||
const parentIdentifier = await this.containerManager.getContainer(identifier);
|
||||
private getRelatedNames(identifier: ResourceIdentifier): { name: NamedNode; parent: NamedNode } {
|
||||
const parentIdentifier = getParentContainer(identifier);
|
||||
const name = namedNode(identifier.path);
|
||||
const parent = namedNode(parentIdentifier.path);
|
||||
return { name, parent };
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import { matchingMediaType } from '../../util/Util';
|
||||
import { checkRequest } from './ConversionUtil';
|
||||
import { validateRequestArgs, matchingMediaType } from './ConversionUtil';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
|
||||
@@ -48,7 +47,7 @@ export class ChainedConverter extends TypedRepresentationConverter {
|
||||
// So we only check if the input can be parsed and the preferred type can be written
|
||||
const inTypes = this.filterTypes(await this.first.getInputTypes());
|
||||
const outTypes = this.filterTypes(await this.last.getOutputTypes());
|
||||
checkRequest(input, inTypes, outTypes);
|
||||
validateRequestArgs(input, inTypes, outTypes);
|
||||
}
|
||||
|
||||
private filterTypes(typeVals: Record<string, number>): string[] {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { RepresentationPreferences } from '../../ldp/representation/Represe
|
||||
import { INTERNAL_ALL } from '../../util/ContentTypes';
|
||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import { matchingMediaType } from '../../util/Util';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
|
||||
/**
|
||||
@@ -57,6 +56,33 @@ RepresentationPreference[] => {
|
||||
return weightedSupported.filter((preference): boolean => preference.weight !== 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given two media types/ranges match each other.
|
||||
* Takes wildcards into account.
|
||||
* @param mediaA - Media type to match.
|
||||
* @param mediaB - Media type to match.
|
||||
*
|
||||
* @returns True if the media type patterns can match each other.
|
||||
*/
|
||||
export const matchingMediaType = (mediaA: string, mediaB: string): boolean => {
|
||||
if (mediaA === mediaB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [ typeA, subTypeA ] = mediaA.split('/');
|
||||
const [ typeB, subTypeB ] = mediaB.split('/');
|
||||
if (typeA === '*' || typeB === '*') {
|
||||
return true;
|
||||
}
|
||||
if (typeA !== typeB) {
|
||||
return false;
|
||||
}
|
||||
if (subTypeA === '*' || subTypeB === '*') {
|
||||
return true;
|
||||
}
|
||||
return subTypeA === subTypeB;
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs some standard checks on the input request:
|
||||
* - Checks if there is a content type for the input.
|
||||
@@ -66,8 +92,8 @@ RepresentationPreference[] => {
|
||||
* @param supportedIn - Media types that can be parsed by the converter.
|
||||
* @param supportedOut - Media types that can be produced by the converter.
|
||||
*/
|
||||
export const checkRequest = (request: RepresentationConverterArgs, supportedIn: string[], supportedOut: string[]):
|
||||
void => {
|
||||
export const validateRequestArgs = (request: RepresentationConverterArgs, supportedIn: string[],
|
||||
supportedOut: string[]): void => {
|
||||
const inType = request.representation.metadata.contentType;
|
||||
if (!inType) {
|
||||
throw new UnsupportedHttpError('Input type required for conversion.');
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RepresentationMetadata } from '../../ldp/representation/RepresentationM
|
||||
import type { RepresentationPreferences } from '../../ldp/representation/RepresentationPreferences';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { CONTENT_TYPE } from '../../util/UriConstants';
|
||||
import { checkRequest, matchingTypes } from './ConversionUtil';
|
||||
import { validateRequestArgs, matchingTypes } from './ConversionUtil';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
|
||||
@@ -22,7 +22,7 @@ export class QuadToRdfConverter extends TypedRepresentationConverter {
|
||||
}
|
||||
|
||||
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
|
||||
checkRequest(input, [ INTERNAL_QUADS ], await rdfSerializer.getContentTypes());
|
||||
validateRequestArgs(input, [ INTERNAL_QUADS ], await rdfSerializer.getContentTypes());
|
||||
}
|
||||
|
||||
public async handle(input: RepresentationConverterArgs): Promise<Representation> {
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { Representation } from '../../ldp/representation/Representation';
|
||||
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import { pipeSafely } from '../../util/StreamUtil';
|
||||
import { CONTENT_TYPE } from '../../util/UriConstants';
|
||||
import { pipeSafe } from '../../util/Util';
|
||||
import { checkRequest } from './ConversionUtil';
|
||||
import { validateRequestArgs } from './ConversionUtil';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
|
||||
@@ -23,7 +23,7 @@ export class RdfToQuadConverter extends TypedRepresentationConverter {
|
||||
}
|
||||
|
||||
public async canHandle(input: RepresentationConverterArgs): Promise<void> {
|
||||
checkRequest(input, await rdfParser.getContentTypes(), [ INTERNAL_QUADS ]);
|
||||
validateRequestArgs(input, await rdfParser.getContentTypes(), [ INTERNAL_QUADS ]);
|
||||
}
|
||||
|
||||
public async handle(input: RepresentationConverterArgs): Promise<Representation> {
|
||||
@@ -40,7 +40,7 @@ export class RdfToQuadConverter extends TypedRepresentationConverter {
|
||||
// Wrap the stream such that errors are transformed
|
||||
// (Node 10 requires both writableObjectMode and readableObjectMode)
|
||||
const pass = new PassThrough({ writableObjectMode: true, readableObjectMode: true });
|
||||
const data = pipeSafe(rawQuads, pass, (error): Error => new UnsupportedHttpError(error.message));
|
||||
const data = pipeSafely(rawQuads, pass, (error): Error => new UnsupportedHttpError(error.message));
|
||||
|
||||
return {
|
||||
binary: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Representation } from '../../ldp/representation/Representation';
|
||||
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
|
||||
import { UnsupportedHttpError } from '../../util/errors/UnsupportedHttpError';
|
||||
import { trimTrailingSlashes } from '../../util/Util';
|
||||
import { trimTrailingSlashes } from '../../util/PathUtil';
|
||||
import type { ResourceStore } from '../ResourceStore';
|
||||
import { RouterRule } from './RouterRule';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { HttpResponse } from '../server/HttpResponse';
|
||||
import { UnsupportedHttpError } from './errors/UnsupportedHttpError';
|
||||
|
||||
const logger = getLoggerFor('HeaderUtil');
|
||||
@@ -355,3 +356,25 @@ export const parseAcceptLanguage = (input: string): AcceptLanguage[] => {
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a header value without overriding previous values.
|
||||
*/
|
||||
export const addHeader = (response: HttpResponse, name: string, value: string | string[]): void => {
|
||||
let allValues: string[] = [];
|
||||
if (response.hasHeader(name)) {
|
||||
let oldValues = response.getHeader(name)!;
|
||||
if (typeof oldValues === 'string') {
|
||||
oldValues = [ oldValues ];
|
||||
} else if (typeof oldValues === 'number') {
|
||||
oldValues = [ `${oldValues}` ];
|
||||
}
|
||||
allValues = oldValues;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
allValues.push(...value);
|
||||
} else {
|
||||
allValues.push(value);
|
||||
}
|
||||
response.setHeader(name, allValues.length === 1 ? allValues[0] : allValues);
|
||||
};
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { Readable } from 'stream';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { DataFactory, StreamParser, StreamWriter } from 'n3';
|
||||
import type { NamedNode, Quad } from 'rdf-js';
|
||||
import streamifyArray from 'streamify-array';
|
||||
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
||||
import { TEXT_TURTLE } from './ContentTypes';
|
||||
import { LDP, RDF } from './UriConstants';
|
||||
import { toNamedNode } from './UriUtil';
|
||||
import { pipeSafe, pushQuad } from './Util';
|
||||
|
||||
export class MetadataController {
|
||||
/**
|
||||
* Helper function to generate type quads for a Container or Resource.
|
||||
* @param subject - Subject for the new quads.
|
||||
* @param isContainer - If the identifier corresponds to a container.
|
||||
*
|
||||
* @returns The generated quads.
|
||||
*/
|
||||
public generateResourceQuads(subject: NamedNode, isContainer: boolean): Quad[] {
|
||||
const quads: Quad[] = [];
|
||||
if (isContainer) {
|
||||
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Container));
|
||||
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.BasicContainer));
|
||||
}
|
||||
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Resource));
|
||||
|
||||
return quads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to generate the quads describing that the resource URIs are children of the container URI.
|
||||
* @param containerURI - The URI of the container.
|
||||
* @param childURIs - The URI of the child resources.
|
||||
*
|
||||
* @returns The generated quads.
|
||||
*/
|
||||
public generateContainerContainsResourceQuads(containerURI: NamedNode, childURIs: string[]): Quad[] {
|
||||
return new RepresentationMetadata(containerURI, { [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for serializing an array of quads, with as result a Readable object.
|
||||
* @param quads - The array of quads.
|
||||
*
|
||||
* @returns The Readable object.
|
||||
*/
|
||||
public serializeQuads(quads: Quad[]): Readable {
|
||||
return pipeSafe(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert a Readable into an array of quads.
|
||||
* @param readable - The readable object.
|
||||
*
|
||||
* @returns A promise containing the array of quads.
|
||||
*/
|
||||
public async parseQuads(readable: Readable): Promise<Quad[]> {
|
||||
return await arrayifyStream(pipeSafe(readable, new StreamParser({ format: TEXT_TURTLE })));
|
||||
}
|
||||
}
|
||||
60
src/util/PathUtil.ts
Normal file
60
src/util/PathUtil.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
|
||||
import { InternalServerError } from './errors/InternalServerError';
|
||||
|
||||
/**
|
||||
* Makes sure the input path has exactly 1 slash at the end.
|
||||
* Multiple slashes will get merged into one.
|
||||
* If there is no slash it will be added.
|
||||
*
|
||||
* @param path - Path to check.
|
||||
*
|
||||
* @returns The potentially changed path.
|
||||
*/
|
||||
export const ensureTrailingSlash = (path: string): string => path.replace(/\/*$/u, '/');
|
||||
|
||||
/**
|
||||
* Makes sure the input path has no slashes at the end.
|
||||
*
|
||||
* @param path - Path to check.
|
||||
*
|
||||
* @returns The potentially changed path.
|
||||
*/
|
||||
export const trimTrailingSlashes = (path: string): string => path.replace(/\/+$/u, '');
|
||||
|
||||
/**
|
||||
* Converts a URI path to the canonical version by splitting on slashes,
|
||||
* decoding any percent-based encodings,
|
||||
* and then encoding any special characters.
|
||||
*/
|
||||
export const toCanonicalUriPath = (path: string): string => path.split('/').map((part): string =>
|
||||
encodeURIComponent(decodeURIComponent(part))).join('/');
|
||||
|
||||
/**
|
||||
* Decodes all components of a URI path.
|
||||
*/
|
||||
export const decodeUriPathComponents = (path: string): string => path.split('/').map(decodeURIComponent).join('/');
|
||||
|
||||
/**
|
||||
* Encodes all (non-slash) special characters in a URI path.
|
||||
*/
|
||||
export const encodeUriPathComponents = (path: string): string => path.split('/').map(encodeURIComponent).join('/');
|
||||
|
||||
/**
|
||||
* Finds the container containing the given resource.
|
||||
* This does not ensure either the container or resource actually exist.
|
||||
*
|
||||
* @param id - Identifier to find container of.
|
||||
*
|
||||
* @returns The identifier of the container this resource is in.
|
||||
*/
|
||||
export const getParentContainer = (id: ResourceIdentifier): ResourceIdentifier => {
|
||||
// Trailing slash is necessary for URL library
|
||||
const parentPath = new URL('..', ensureTrailingSlash(id.path)).toString();
|
||||
|
||||
// This probably means there is an issue with the root
|
||||
if (parentPath === id.path) {
|
||||
throw new InternalServerError('URL root reached');
|
||||
}
|
||||
|
||||
return { path: parentPath };
|
||||
};
|
||||
32
src/util/QuadUtil.ts
Normal file
32
src/util/QuadUtil.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Readable } from 'stream';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { DataFactory, StreamParser, StreamWriter } from 'n3';
|
||||
import type { Literal, NamedNode, Quad } from 'rdf-js';
|
||||
import streamifyArray from 'streamify-array';
|
||||
import { TEXT_TURTLE } from './ContentTypes';
|
||||
import { pipeSafely } from './StreamUtil';
|
||||
|
||||
/**
|
||||
* Generates a quad with the given subject/predicate/object and pushes it to the given array.
|
||||
*/
|
||||
export const pushQuad =
|
||||
(quads: Quad[], subject: NamedNode, predicate: NamedNode, object: NamedNode | Literal): number =>
|
||||
quads.push(DataFactory.quad(subject, predicate, object));
|
||||
|
||||
/**
|
||||
* Helper function for serializing an array of quads, with as result a Readable object.
|
||||
* @param quads - The array of quads.
|
||||
*
|
||||
* @returns The Readable object.
|
||||
*/
|
||||
export const serializeQuads = (quads: Quad[]): Readable =>
|
||||
pipeSafely(streamifyArray(quads), new StreamWriter({ format: TEXT_TURTLE }));
|
||||
|
||||
/**
|
||||
* Helper function to convert a Readable into an array of quads.
|
||||
* @param readable - The readable object.
|
||||
*
|
||||
* @returns A promise containing the array of quads.
|
||||
*/
|
||||
export const parseQuads = async(readable: Readable): Promise<Quad[]> =>
|
||||
arrayifyStream(pipeSafely(readable, new StreamParser({ format: TEXT_TURTLE })));
|
||||
34
src/util/ResourceUtil.ts
Normal file
34
src/util/ResourceUtil.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { DataFactory } from 'n3';
|
||||
import type { NamedNode, Quad } from 'rdf-js';
|
||||
import { RepresentationMetadata } from '../ldp/representation/RepresentationMetadata';
|
||||
import { pushQuad } from './QuadUtil';
|
||||
import { LDP, RDF } from './UriConstants';
|
||||
import { toNamedNode } from './UriUtil';
|
||||
|
||||
/**
|
||||
* Helper function to generate type quads for a Container or Resource.
|
||||
* @param subject - Subject for the new quads.
|
||||
* @param isContainer - If the identifier corresponds to a container.
|
||||
*
|
||||
* @returns The generated quads.
|
||||
*/
|
||||
export const generateResourceQuads = (subject: NamedNode, isContainer: boolean): Quad[] => {
|
||||
const quads: Quad[] = [];
|
||||
if (isContainer) {
|
||||
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Container));
|
||||
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.BasicContainer));
|
||||
}
|
||||
pushQuad(quads, subject, toNamedNode(RDF.type), toNamedNode(LDP.Resource));
|
||||
|
||||
return quads;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to generate the quads describing that the resource URIs are children of the container URI.
|
||||
* @param containerURI - The URI of the container.
|
||||
* @param childURIs - The URI of the child resources.
|
||||
*
|
||||
* @returns The generated quads.
|
||||
*/
|
||||
export const generateContainmentQuads = (containerURI: NamedNode, childURIs: string[]): Quad[] =>
|
||||
new RepresentationMetadata(containerURI, { [LDP.contains]: childURIs.map(DataFactory.namedNode) }).quads();
|
||||
38
src/util/StreamUtil.ts
Normal file
38
src/util/StreamUtil.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Readable, Writable } from 'stream';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
|
||||
const logger = getLoggerFor('StreamUtil');
|
||||
|
||||
/**
|
||||
* Joins all strings of a stream.
|
||||
* @param stream - Stream of strings.
|
||||
*
|
||||
* @returns The joined string.
|
||||
*/
|
||||
export const readableToString = async(stream: Readable): Promise<string> => (await arrayifyStream(stream)).join('');
|
||||
|
||||
/**
|
||||
* Pipes one stream into another and emits errors of the first stream with the second.
|
||||
* In case of an error in the first stream the second one will be destroyed with the given error.
|
||||
* @param readable - Initial readable stream.
|
||||
* @param destination - The destination for writing data.
|
||||
* @param mapError - Optional function that takes the error and converts it to a new error.
|
||||
*
|
||||
* @returns The destination stream.
|
||||
*/
|
||||
export const pipeSafely = <T extends Writable>(readable: NodeJS.ReadableStream, destination: T,
|
||||
mapError?: (error: Error) => Error): T => {
|
||||
// Not using `stream.pipeline` since the result there only emits an error event if the last stream has the error
|
||||
readable.pipe(destination);
|
||||
readable.on('error', (error): void => {
|
||||
logger.warn(`Piped stream errored with ${error.message}`);
|
||||
|
||||
// From https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options :
|
||||
// "One important caveat is that if the Readable stream emits an error during processing, the Writable destination
|
||||
// is not closed automatically. If an error occurs, it will be necessary to manually close each stream
|
||||
// in order to prevent memory leaks."
|
||||
destination.destroy(mapError ? mapError(error) : error);
|
||||
});
|
||||
return destination;
|
||||
};
|
||||
131
src/util/Util.ts
131
src/util/Util.ts
@@ -1,131 +0,0 @@
|
||||
import type { Readable, Writable } from 'stream';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { DataFactory } from 'n3';
|
||||
import type { Literal, NamedNode, Quad } from 'rdf-js';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { HttpResponse } from '../server/HttpResponse';
|
||||
|
||||
const logger = getLoggerFor('Util');
|
||||
|
||||
/**
|
||||
* Makes sure the input path has exactly 1 slash at the end.
|
||||
* Multiple slashes will get merged into one.
|
||||
* If there is no slash it will be added.
|
||||
*
|
||||
* @param path - Path to check.
|
||||
*
|
||||
* @returns The potentially changed path.
|
||||
*/
|
||||
export const ensureTrailingSlash = (path: string): string => path.replace(/\/*$/u, '/');
|
||||
|
||||
/**
|
||||
* Joins all strings of a stream.
|
||||
* @param stream - Stream of strings.
|
||||
*
|
||||
* @returns The joined string.
|
||||
*/
|
||||
export const readableToString = async(stream: Readable): Promise<string> => (await arrayifyStream(stream)).join('');
|
||||
|
||||
/**
|
||||
* Makes sure the input path has no slashes at the end.
|
||||
*
|
||||
* @param path - Path to check.
|
||||
*
|
||||
* @returns The potentially changed path.
|
||||
*/
|
||||
export const trimTrailingSlashes = (path: string): string => path.replace(/\/+$/u, '');
|
||||
|
||||
/**
|
||||
* Checks if the given two media types/ranges match each other.
|
||||
* Takes wildcards into account.
|
||||
* @param mediaA - Media type to match.
|
||||
* @param mediaB - Media type to match.
|
||||
*
|
||||
* @returns True if the media type patterns can match each other.
|
||||
*/
|
||||
export const matchingMediaType = (mediaA: string, mediaB: string): boolean => {
|
||||
const [ typeA, subTypeA ] = mediaA.split('/');
|
||||
const [ typeB, subTypeB ] = mediaB.split('/');
|
||||
if (typeA === '*' || typeB === '*') {
|
||||
return true;
|
||||
}
|
||||
if (typeA !== typeB) {
|
||||
return false;
|
||||
}
|
||||
if (subTypeA === '*' || subTypeB === '*') {
|
||||
return true;
|
||||
}
|
||||
return subTypeA === subTypeB;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pipes one stream into another and emits errors of the first stream with the second.
|
||||
* In case of an error in the first stream the second one will be destroyed with the given error.
|
||||
* @param readable - Initial readable stream.
|
||||
* @param destination - The destination for writing data.
|
||||
* @param mapError - Optional function that takes the error and converts it to a new error.
|
||||
*
|
||||
* @returns The destination stream.
|
||||
*/
|
||||
export const pipeSafe = <T extends Writable>(readable: NodeJS.ReadableStream, destination: T,
|
||||
mapError?: (error: Error) => Error): T => {
|
||||
// Not using `stream.pipeline` since the result there only emits an error event if the last stream has the error
|
||||
readable.pipe(destination);
|
||||
readable.on('error', (error): void => {
|
||||
logger.warn(`Piped stream errored with ${error.message}`);
|
||||
|
||||
// From https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options :
|
||||
// "One important caveat is that if the Readable stream emits an error during processing, the Writable destination
|
||||
// is not closed automatically. If an error occurs, it will be necessary to manually close each stream
|
||||
// in order to prevent memory leaks."
|
||||
destination.destroy(mapError ? mapError(error) : error);
|
||||
});
|
||||
return destination;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a URI path to the canonical version by splitting on slashes,
|
||||
* decoding any percent-based encodings,
|
||||
* and then encoding any special characters.
|
||||
*/
|
||||
export const toCanonicalUriPath = (path: string): string => path.split('/').map((part): string =>
|
||||
encodeURIComponent(decodeURIComponent(part))).join('/');
|
||||
|
||||
/**
|
||||
* Decodes all components of a URI path.
|
||||
*/
|
||||
export const decodeUriPathComponents = (path: string): string => path.split('/').map(decodeURIComponent).join('/');
|
||||
|
||||
/**
|
||||
* Encodes all (non-slash) special characters in a URI path.
|
||||
*/
|
||||
export const encodeUriPathComponents = (path: string): string => path.split('/').map(encodeURIComponent).join('/');
|
||||
|
||||
/**
|
||||
* Generates a quad with the given subject/predicate/object and pushes it to the given array.
|
||||
*/
|
||||
export const pushQuad =
|
||||
(quads: Quad[], subject: NamedNode, predicate: NamedNode, object: NamedNode | Literal): number =>
|
||||
quads.push(DataFactory.quad(subject, predicate, object));
|
||||
|
||||
/**
|
||||
* Adds a header value without overriding previous values.
|
||||
*/
|
||||
export const addHeader = (response: HttpResponse, name: string, value: string | string[]): void => {
|
||||
let allValues: string[] = [];
|
||||
if (response.hasHeader(name)) {
|
||||
let oldValues = response.getHeader(name)!;
|
||||
if (typeof oldValues === 'string') {
|
||||
oldValues = [ oldValues ];
|
||||
} else if (typeof oldValues === 'number') {
|
||||
oldValues = [ `${oldValues}` ];
|
||||
}
|
||||
allValues = oldValues;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
allValues.push(...value);
|
||||
} else {
|
||||
allValues.push(value);
|
||||
}
|
||||
response.setHeader(name, allValues.length === 1 ? allValues[0] : allValues);
|
||||
};
|
||||
Reference in New Issue
Block a user