feat(integration): improve integration test connection (#3005)
This commit is contained in:
88
packages/integrations/src/base/errors/decorator.ts
Normal file
88
packages/integrations/src/base/errors/decorator.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { isFunction } from "@homarr/common";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import type { Integration } from "../integration";
|
||||
import type { IIntegrationErrorHandler } from "./handler";
|
||||
import { IntegrationError } from "./integration-error";
|
||||
import { IntegrationUnknownError } from "./integration-unknown-error";
|
||||
|
||||
const localLogger = logger.child({
|
||||
module: "HandleIntegrationErrors",
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-explicit-any
|
||||
type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T;
|
||||
|
||||
export const HandleIntegrationErrors = (errorHandlers: IIntegrationErrorHandler[]) => {
|
||||
return <T extends AbstractConstructor<Integration>>(IntegrationBaseClass: T): T => {
|
||||
abstract class ErrorHandledIntegration extends IntegrationBaseClass {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(...args: any[]) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
super(...args);
|
||||
const processedProperties = new Set<string>();
|
||||
|
||||
let currentProto: unknown = Object.getPrototypeOf(this);
|
||||
|
||||
while (currentProto && currentProto !== Object.prototype) {
|
||||
for (const propertyKey of Object.getOwnPropertyNames(currentProto)) {
|
||||
if (propertyKey === "constructor" || processedProperties.has(propertyKey)) continue;
|
||||
|
||||
const descriptor = Object.getOwnPropertyDescriptor(currentProto, propertyKey);
|
||||
|
||||
if (!descriptor) continue;
|
||||
const original: unknown = descriptor.value;
|
||||
if (!isFunction(original)) continue;
|
||||
|
||||
processedProperties.add(propertyKey);
|
||||
|
||||
const wrapped = (...methodArgs: unknown[]) => {
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof IntegrationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const handler of errorHandlers) {
|
||||
const handledError = handler.handleError(error, this.publicIntegration);
|
||||
if (!handledError) continue;
|
||||
|
||||
throw handledError;
|
||||
}
|
||||
|
||||
// If the error was handled and should be thrown again, throw it
|
||||
localLogger.debug("Unhandled error in integration", {
|
||||
error: error instanceof Error ? `${error.name}: ${error.message}` : undefined,
|
||||
integrationName: this.publicIntegration.name,
|
||||
});
|
||||
throw new IntegrationUnknownError(this.publicIntegration, { cause: error });
|
||||
};
|
||||
|
||||
try {
|
||||
const result = original.apply(this, methodArgs);
|
||||
|
||||
if (result instanceof Promise) {
|
||||
return result.catch((error: unknown) => {
|
||||
handleError(error);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(this, propertyKey, {
|
||||
...descriptor,
|
||||
value: wrapped,
|
||||
});
|
||||
}
|
||||
|
||||
currentProto = Object.getPrototypeOf(currentProto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorHandledIntegration;
|
||||
};
|
||||
};
|
||||
5
packages/integrations/src/base/errors/handler.ts
Normal file
5
packages/integrations/src/base/errors/handler.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { IntegrationError, IntegrationErrorData } from "./integration-error";
|
||||
|
||||
export interface IIntegrationErrorHandler {
|
||||
handleError(error: unknown, integration: IntegrationErrorData): IntegrationError | undefined;
|
||||
}
|
||||
13
packages/integrations/src/base/errors/http/index.ts
Normal file
13
packages/integrations/src/base/errors/http/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {
|
||||
AxiosHttpErrorHandler,
|
||||
FetchHttpErrorHandler,
|
||||
OFetchHttpErrorHandler,
|
||||
TsdavHttpErrorHandler,
|
||||
} from "@homarr/common/server";
|
||||
|
||||
import { IntegrationHttpErrorHandler } from "./integration-http-error-handler";
|
||||
|
||||
export const integrationFetchHttpErrorHandler = new IntegrationHttpErrorHandler(new FetchHttpErrorHandler());
|
||||
export const integrationOFetchHttpErrorHandler = new IntegrationHttpErrorHandler(new OFetchHttpErrorHandler());
|
||||
export const integrationAxiosHttpErrorHandler = new IntegrationHttpErrorHandler(new AxiosHttpErrorHandler());
|
||||
export const integrationTsdavHttpErrorHandler = new IntegrationHttpErrorHandler(new TsdavHttpErrorHandler());
|
||||
@@ -0,0 +1,26 @@
|
||||
import { RequestError, ResponseError } from "@homarr/common/server";
|
||||
import type { HttpErrorHandler } from "@homarr/common/server";
|
||||
|
||||
import type { IIntegrationErrorHandler } from "../handler";
|
||||
import type { IntegrationError, IntegrationErrorData } from "../integration-error";
|
||||
import { IntegrationRequestError } from "./integration-request-error";
|
||||
import { IntegrationResponseError } from "./integration-response-error";
|
||||
|
||||
export class IntegrationHttpErrorHandler implements IIntegrationErrorHandler {
|
||||
private readonly httpErrorHandler: HttpErrorHandler;
|
||||
|
||||
constructor(httpErrorHandler: HttpErrorHandler) {
|
||||
this.httpErrorHandler = httpErrorHandler;
|
||||
}
|
||||
|
||||
handleError(error: unknown, integration: IntegrationErrorData): IntegrationError | undefined {
|
||||
if (error instanceof RequestError) return new IntegrationRequestError(integration, { cause: error });
|
||||
if (error instanceof ResponseError) return new IntegrationResponseError(integration, { cause: error });
|
||||
|
||||
const requestError = this.httpErrorHandler.handleRequestError(error);
|
||||
if (requestError) return new IntegrationRequestError(integration, { cause: requestError });
|
||||
const responseError = this.httpErrorHandler.handleResponseError(error);
|
||||
if (responseError) return new IntegrationResponseError(integration, { cause: responseError });
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { AnyRequestError, RequestError, RequestErrorType } from "@homarr/common/server";
|
||||
|
||||
import type { IntegrationErrorData } from "../integration-error";
|
||||
import { IntegrationError } from "../integration-error";
|
||||
|
||||
export type IntegrationRequestErrorOfType<TType extends RequestErrorType> = IntegrationRequestError & {
|
||||
cause: RequestError<TType>;
|
||||
};
|
||||
|
||||
export class IntegrationRequestError extends IntegrationError {
|
||||
constructor(integration: IntegrationErrorData, { cause }: { cause: AnyRequestError }) {
|
||||
super(integration, "Request to integration failed", { cause });
|
||||
this.name = IntegrationRequestError.name;
|
||||
}
|
||||
|
||||
get cause(): AnyRequestError {
|
||||
return super.cause as AnyRequestError;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ResponseError } from "@homarr/common/server";
|
||||
|
||||
import type { IntegrationErrorData } from "../integration-error";
|
||||
import { IntegrationError } from "../integration-error";
|
||||
|
||||
export class IntegrationResponseError extends IntegrationError {
|
||||
constructor(integration: IntegrationErrorData, { cause }: { cause: ResponseError }) {
|
||||
super(integration, "Response from integration did not indicate success", { cause });
|
||||
this.name = IntegrationResponseError.name;
|
||||
}
|
||||
|
||||
get cause(): ResponseError {
|
||||
return super.cause as ResponseError;
|
||||
}
|
||||
}
|
||||
18
packages/integrations/src/base/errors/integration-error.ts
Normal file
18
packages/integrations/src/base/errors/integration-error.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface IntegrationErrorData {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export abstract class IntegrationError extends Error {
|
||||
public readonly integrationId: string;
|
||||
public readonly integrationName: string;
|
||||
public readonly integrationUrl: string;
|
||||
|
||||
constructor(integration: IntegrationErrorData, message: string, { cause }: ErrorOptions) {
|
||||
super(message, { cause });
|
||||
this.integrationId = integration.id;
|
||||
this.integrationName = integration.name;
|
||||
this.integrationUrl = integration.url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { IntegrationErrorData } from "./integration-error";
|
||||
import { IntegrationError } from "./integration-error";
|
||||
|
||||
export class IntegrationUnknownError extends IntegrationError {
|
||||
constructor(integration: IntegrationErrorData, { cause }: ErrorOptions) {
|
||||
super(integration, "An unknown error occured while executing Integration method", { cause });
|
||||
}
|
||||
}
|
||||
6
packages/integrations/src/base/errors/parse/index.ts
Normal file
6
packages/integrations/src/base/errors/parse/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { JsonParseErrorHandler, ZodParseErrorHandler } from "@homarr/common/server";
|
||||
|
||||
import { IntegrationParseErrorHandler } from "./integration-parse-error-handler";
|
||||
|
||||
export const integrationZodParseErrorHandler = new IntegrationParseErrorHandler(new ZodParseErrorHandler());
|
||||
export const integrationJsonParseErrorHandler = new IntegrationParseErrorHandler(new JsonParseErrorHandler());
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ParseError } from "@homarr/common/server";
|
||||
import type { ParseErrorHandler } from "@homarr/common/server";
|
||||
|
||||
import type { IIntegrationErrorHandler } from "../handler";
|
||||
import type { IntegrationError, IntegrationErrorData } from "../integration-error";
|
||||
import { IntegrationParseError } from "./integration-parse-error";
|
||||
|
||||
export class IntegrationParseErrorHandler implements IIntegrationErrorHandler {
|
||||
private readonly parseErrorHandler: ParseErrorHandler;
|
||||
|
||||
constructor(parseErrorHandler: ParseErrorHandler) {
|
||||
this.parseErrorHandler = parseErrorHandler;
|
||||
}
|
||||
|
||||
handleError(error: unknown, integration: IntegrationErrorData): IntegrationError | undefined {
|
||||
if (error instanceof ParseError) return new IntegrationParseError(integration, { cause: error });
|
||||
const parseError = this.parseErrorHandler.handleParseError(error);
|
||||
if (parseError) return new IntegrationParseError(integration, { cause: parseError });
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { ParseError } from "@homarr/common/server";
|
||||
|
||||
import type { IntegrationErrorData } from "../integration-error";
|
||||
import { IntegrationError } from "../integration-error";
|
||||
|
||||
export class IntegrationParseError extends IntegrationError {
|
||||
constructor(integration: IntegrationErrorData, { cause }: { cause: ParseError }) {
|
||||
super(integration, "Failed to parse integration data", { cause });
|
||||
this.name = IntegrationParseError.name;
|
||||
}
|
||||
|
||||
get cause(): ParseError {
|
||||
return super.cause as ParseError;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user