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:
@@ -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