Merge branch 'main' into versions/3.0.0

This commit is contained in:
Joachim Van Herwegen
2021-11-22 09:20:41 +01:00
32 changed files with 360 additions and 223 deletions

View File

@@ -40,25 +40,25 @@ export class SparqlPatchModesExtractor extends ModesExtractor {
return Boolean((data as SparqlUpdatePatch).algebra);
}
private isSupported(op: Algebra.Operation): boolean {
private isSupported(op: Algebra.Update): boolean {
if (this.isDeleteInsert(op) || this.isNop(op)) {
return true;
}
if (op.type === Algebra.types.COMPOSITE_UPDATE) {
return (op as Algebra.CompositeUpdate).updates.every((update): boolean => this.isSupported(update));
return op.updates.every((update): boolean => this.isSupported(update));
}
return false;
}
private isDeleteInsert(op: Algebra.Operation): op is Algebra.DeleteInsert {
private isDeleteInsert(op: Algebra.Update): op is Algebra.DeleteInsert {
return op.type === Algebra.types.DELETE_INSERT;
}
private isNop(op: Algebra.Operation): op is Algebra.Nop {
private isNop(op: Algebra.Update): op is Algebra.Nop {
return op.type === Algebra.types.NOP;
}
private needsAppend(update: Algebra.Operation): boolean {
private needsAppend(update: Algebra.Update): boolean {
if (this.isNop(update)) {
return false;
}
@@ -69,7 +69,7 @@ export class SparqlPatchModesExtractor extends ModesExtractor {
return (update as Algebra.CompositeUpdate).updates.some((op): boolean => this.needsAppend(op));
}
private needsWrite(update: Algebra.Operation): boolean {
private needsWrite(update: Algebra.Update): boolean {
if (this.isNop(update)) {
return false;
}

View File

@@ -1,5 +1,5 @@
import { EventEmitter } from 'events';
import type WebSocket from 'ws';
import type { WebSocket } from 'ws';
import { getLoggerFor } from '../logging/LogUtil';
import type { HttpRequest } from '../server/HttpRequest';
import { WebSocketHandler } from '../server/WebSocketHandler';

View File

@@ -26,7 +26,7 @@ export class SparqlUpdateBodyParser extends BodyParser {
const sparql = await readableToString(request);
let algebra: Algebra.Operation;
try {
algebra = translate(sparql, { quads: true, baseIRI: metadata.identifier.value });
algebra = translate(sparql, { quads: true, baseIRI: metadata.identifier.value }) as Algebra.Update;
} catch (error: unknown) {
this.logger.warn('Could not translate SPARQL query to SPARQL algebra', { error });
throw new BadRequestHttpError(createErrorMessage(error), { cause: error });

View File

@@ -3,7 +3,7 @@ import type {
RepresentationConverterArgs,
} from '../../../storage/conversion/RepresentationConverter';
import { INTERNAL_ERROR } from '../../../util/ContentTypes';
import { getStatusCode } from '../../../util/errors/ErrorUtil';
import { getStatusCode } from '../../../util/errors/HttpErrorUtil';
import { toLiteral } from '../../../util/TermUtil';
import { HTTP, XSD } from '../../../util/Vocabularies';
import { BasicRepresentation } from '../../representation/BasicRepresentation';

View File

@@ -1,5 +1,6 @@
import { getLoggerFor } from '../../../logging/LogUtil';
import { createErrorMessage, getStatusCode } from '../../../util/errors/ErrorUtil';
import { createErrorMessage } from '../../../util/errors/ErrorUtil';
import { getStatusCode } from '../../../util/errors/HttpErrorUtil';
import { guardedStreamFrom } from '../../../util/StreamUtil';
import { toLiteral } from '../../../util/TermUtil';
import { HTTP, XSD } from '../../../util/Vocabularies';

View File

@@ -320,6 +320,7 @@ export * from './util/errors/ConflictHttpError';
export * from './util/errors/ErrorUtil';
export * from './util/errors/ForbiddenHttpError';
export * from './util/errors/HttpError';
export * from './util/errors/HttpErrorUtil';
export * from './util/errors/InternalServerError';
export * from './util/errors/MethodNotAllowedHttpError';
export * from './util/errors/NotFoundHttpError';

View File

@@ -1,4 +1,4 @@
import type WebSocket from 'ws';
import type { WebSocket } from 'ws';
import { AsyncHandler } from '../util/handlers/AsyncHandler';
import type { HttpRequest } from './HttpRequest';

View File

@@ -1,6 +1,6 @@
import type { Server } from 'http';
import type { Socket } from 'net';
import type WebSocket from 'ws';
import type { WebSocket } from 'ws';
import { Server as WebSocketServer } from 'ws';
import type { HttpRequest } from './HttpRequest';
import type { HttpServerFactory } from './HttpServerFactory';

View File

@@ -1,5 +1,7 @@
import { createAggregateError } from './errors/HttpErrorUtil';
// eslint-disable-next-line @typescript-eslint/no-empty-function
const infinitePromise = new Promise<boolean>((): void => {});
function noop(): void {}
/**
* A function that simulates the Array.some behaviour but on an array of Promises.
@@ -14,17 +16,36 @@ const infinitePromise = new Promise<boolean>((): void => {});
* 2. throwing an error should be logically equivalent to returning false.
*/
export async function promiseSome(predicates: Promise<boolean>[]): Promise<boolean> {
// These promises will only finish when their predicate returns true
const infinitePredicates = predicates.map(async(predicate): Promise<boolean> => predicate.then(
async(value): Promise<boolean> => value ? true : infinitePromise,
async(): Promise<boolean> => infinitePromise,
));
// Returns after all predicates are resolved
const finalPromise = Promise.allSettled(predicates).then((results): boolean =>
results.some((result): boolean => result.status === 'fulfilled' && result.value));
// Either one of the infinitePredicates will return true,
// or finalPromise will return the result if none of them did or finalPromise was faster
return Promise.race([ ...infinitePredicates, finalPromise ]);
return new Promise((resolve): void => {
function resolveIfTrue(value: boolean): void {
if (value) {
resolve(true);
}
}
Promise.all(predicates.map((predicate): Promise<void> => predicate.then(resolveIfTrue, noop)))
.then((): void => resolve(false), noop);
});
}
/**
* Obtains the values of all fulfilled promises.
* If there are rejections (and `ignoreErrors` is false), throws a combined error of all rejected promises.
*/
export async function allFulfilled<T>(promises: Promise<T> [], ignoreErrors = false): Promise<T[]> {
// Collect values and errors
const values: T[] = [];
const errors: Error[] = [];
for (const result of await Promise.allSettled(promises)) {
if (result.status === 'fulfilled') {
values.push(result.value);
} else if (!ignoreErrors) {
errors.push(result.reason);
}
}
// Either throw or return
if (errors.length > 0) {
throw createAggregateError(errors);
}
return values;
}

View File

@@ -1,5 +1,4 @@
import { types } from 'util';
import { HttpError } from './HttpError';
/**
* Checks if the input is an {@link Error}.
@@ -25,10 +24,3 @@ export function assertError(error: unknown): asserts error is Error {
export function createErrorMessage(error: unknown): string {
return isError(error) ? error.message : `Unknown error: ${error}`;
}
/**
* Returns the HTTP status code corresponding to the error.
*/
export function getStatusCode(error: Error): number {
return HttpError.isInstance(error) ? error.statusCode : 500;
}

View File

@@ -0,0 +1,38 @@
import { BadRequestHttpError } from './BadRequestHttpError';
import { createErrorMessage } from './ErrorUtil';
import { HttpError } from './HttpError';
import { InternalServerError } from './InternalServerError';
/**
* Returns the HTTP status code corresponding to the error.
*/
export function getStatusCode(error: Error): number {
return HttpError.isInstance(error) ? error.statusCode : 500;
}
/**
* Combines a list of errors into a single HttpErrors.
* Status code depends on the input errors. If they all share the same status code that code will be re-used.
* If they are all within the 4xx range, 400 will be used, otherwise 500.
*
* @param errors - Errors to combine.
* @param messagePrefix - Prefix for the aggregate error message. Will be followed with an array of all the messages.
*/
export function createAggregateError(errors: Error[], messagePrefix = 'No handler supports the given input:'):
HttpError {
const httpErrors = errors.map((error): HttpError =>
HttpError.isInstance(error) ? error : new InternalServerError(createErrorMessage(error)));
const joined = httpErrors.map((error: Error): string => error.message).join(', ');
const message = `${messagePrefix} [${joined}]`;
// Check if all errors have the same status code
if (httpErrors.length > 0 && httpErrors.every((error): boolean => error.statusCode === httpErrors[0].statusCode)) {
return new HttpError(httpErrors[0].statusCode, httpErrors[0].name, message);
}
// Find the error range (4xx or 5xx)
if (httpErrors.some((error): boolean => error.statusCode >= 500)) {
return new InternalServerError(message);
}
return new BadRequestHttpError(message);
}

View File

@@ -1,36 +1,7 @@
import { BadRequestHttpError } from '../errors/BadRequestHttpError';
import { createErrorMessage, isError } from '../errors/ErrorUtil';
import { HttpError } from '../errors/HttpError';
import { InternalServerError } from '../errors/InternalServerError';
import { createAggregateError } from '../errors/HttpErrorUtil';
import type { AsyncHandler } from './AsyncHandler';
/**
* Combines a list of errors into a single HttpErrors.
* Status code depends on the input errors. If they all share the same status code that code will be re-used.
* If they are all within the 4xx range, 400 will be used, otherwise 500.
*
* @param errors - Errors to combine.
* @param messagePrefix - Prefix for the aggregate error message. Will be followed with an array of all the messages.
*/
export function createAggregateError(errors: Error[], messagePrefix = 'No handler supports the given input:'):
HttpError {
const httpErrors = errors.map((error): HttpError =>
HttpError.isInstance(error) ? error : new InternalServerError(createErrorMessage(error)));
const joined = httpErrors.map((error: Error): string => error.message).join(', ');
const message = `${messagePrefix} [${joined}]`;
// Check if all errors have the same status code
if (httpErrors.length > 0 && httpErrors.every((error): boolean => error.statusCode === httpErrors[0].statusCode)) {
return new HttpError(httpErrors[0].statusCode, httpErrors[0].name, message);
}
// Find the error range (4xx or 5xx)
if (httpErrors.some((error): boolean => error.statusCode >= 500)) {
return new InternalServerError(message);
}
return new BadRequestHttpError(message);
}
/**
* Finds a handler that can handle the given input data.
* Otherwise an error gets thrown.

View File

@@ -12,12 +12,10 @@ export class ParallelHandler<TIn = void, TOut = void> extends AsyncHandler<TIn,
}
public async canHandle(input: TIn): Promise<void> {
// eslint-disable-next-line @typescript-eslint/promise-function-async
await Promise.all(this.handlers.map((handler): Promise<void> => handler.canHandle(input)));
}
public async handle(input: TIn): Promise<TOut[]> {
// eslint-disable-next-line @typescript-eslint/promise-function-async
return Promise.all(this.handlers.map((handler): Promise<TOut> => handler.handle(input)));
}
}

View File

@@ -1,31 +1,41 @@
import { allFulfilled } from '../PromiseUtil';
import { AsyncHandler } from './AsyncHandler';
import { createAggregateError, filterHandlers, findHandler } from './HandlerUtil';
import { filterHandlers, findHandler } from './HandlerUtil';
// Helper types to make sure the UnionHandler has the same in/out types as the AsyncHandler type it wraps
type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
type InType<T extends AsyncHandler<any, any>> = Parameters<T['handle']>[0];
type OutType<T extends AsyncHandler<any, any>> = ThenArg<ReturnType<T['handle']>>;
type HandlerType<T extends AsyncHandler> = AsyncHandler<InType<T>, OutType<T>>;
type OutType<T extends AsyncHandler<any, any>> = Awaited<ReturnType<T['handle']>>;
/**
* Utility handler that allows combining the results of multiple handlers into one.
* Will run all the handlers and then call the abstract `combine` function with the results,
* which should return the output of the class.
*
* If `requireAll` is true, the handler will fail if any of the handlers do not support the input.
* If `requireAll` is false, only the handlers that support the input will be called,
* only if all handlers reject the input will this handler reject as well.
* With `requireAll` set to false, the length of the input array
* for the `combine` function is variable (but always at least 1).
* Will run the handlers and then call the abstract `combine` function with the results,
* which then generates the handler's output.
*/
export abstract class UnionHandler<T extends AsyncHandler<any, any>> extends AsyncHandler<InType<T>, OutType<T>> {
protected readonly handlers: T[];
private readonly requireAll: boolean;
private readonly ignoreErrors: boolean;
protected constructor(handlers: T[], requireAll = false) {
/**
* Creates a new `UnionHandler`.
*
* When `requireAll` is false or `ignoreErrors` is true,
* the length of the input to `combine` can vary;
* otherwise, it is exactly the number of handlers.
*
* @param handlers - The handlers whose output is to be combined.
* @param requireAll - If true, will fail if any of the handlers do not support the input.
If false, only the handlers that support the input will be called;
* will fail only if none of the handlers can handle the input.
* @param ignoreErrors - If true, ignores handlers that fail by omitting their output;
* if false, fails when any handlers fail.
*/
public constructor(handlers: T[], requireAll = false, ignoreErrors = !requireAll) {
super();
this.handlers = handlers;
this.requireAll = requireAll;
this.ignoreErrors = ignoreErrors;
}
public async canHandle(input: InType<T>): Promise<void> {
@@ -38,57 +48,21 @@ export abstract class UnionHandler<T extends AsyncHandler<any, any>> extends Asy
}
public async handle(input: InType<T>): Promise<OutType<T>> {
let handlers: HandlerType<T>[];
if (this.requireAll) {
// Handlers were already checked in canHandle
// eslint-disable-next-line prefer-destructuring
handlers = this.handlers;
} else {
handlers = await filterHandlers(this.handlers, input);
}
const results = await Promise.all(
handlers.map(async(handler): Promise<OutType<T>> => handler.handle(input)),
);
return this.combine(results);
}
public async handleSafe(input: InType<T>): Promise<OutType<T>> {
let handlers: HandlerType<T>[];
if (this.requireAll) {
await this.allCanHandle(input);
// eslint-disable-next-line prefer-destructuring
handlers = this.handlers;
} else {
// This will error if no handler supports the input
handlers = await filterHandlers(this.handlers, input);
}
const results = await Promise.all(
handlers.map(async(handler): Promise<OutType<T>> => handler.handle(input)),
);
return this.combine(results);
const handlers = this.requireAll ? this.handlers : await filterHandlers(this.handlers, input);
const results = handlers.map((handler): Promise<OutType<T>> => handler.handle(input));
return this.combine(await allFulfilled(results, this.ignoreErrors));
}
/**
* Checks if all handlers can handle the input.
* If not, throw an error based on the errors of the failed handlers.
*/
private async allCanHandle(input: InType<T>): Promise<void> {
const results = await Promise.allSettled(this.handlers.map(async(handler): Promise<HandlerType<T>> => {
await handler.canHandle(input);
return handler;
}));
if (results.some(({ status }): boolean => status === 'rejected')) {
const errors = results.map((result): Error => (result as PromiseRejectedResult).reason);
throw createAggregateError(errors);
}
protected async allCanHandle(input: InType<T>): Promise<void> {
await allFulfilled(this.handlers.map((handler): Promise<void> => handler.canHandle(input)));
}
/**
* Combine the results of the handlers into a single output.
* Combines the results of the handlers into a single output.
*/
protected abstract combine(results: OutType<T>[]): Promise<OutType<T>>;
}